From c5fb3aa107fe6eb7366d76a7c32ea0df2fa4cbcb Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sat, 9 Mar 2024 10:37:50 +0100 Subject: [PATCH 001/106] chore(ver): remove useless version number in header --- devkitsnes/readme.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/devkitsnes/readme.txt b/devkitsnes/readme.txt index 141d8e88..2aef093e 100644 --- a/devkitsnes/readme.txt +++ b/devkitsnes/readme.txt @@ -1,6 +1,4 @@ - DevkitSnes 4.1.1 - - Programming Compiler and Tools for Snes +Programming Compiler and Tools for Snes INTRODUCTION ---------------------- From 593aa946de607e648e0897347cb53726184ba420 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sat, 9 Mar 2024 10:41:06 +0100 Subject: [PATCH 002/106] fix(space): add space in example between arguments --- tools/gfx4snes/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/gfx4snes/readme.md b/tools/gfx4snes/readme.md index caaaf663..ca7f5b0c 100644 --- a/tools/gfx4snes/readme.md +++ b/tools/gfx4snes/readme.md @@ -54,7 +54,7 @@ where filename is a 256 color PNG or BMP file ## Example ``` -gfx4snes -o16 -s8 -c16 -e0 -fpng -p -m -b -i myimage.png +gfx4snes -o 16 -s 8 -c 16 -e 0 -f png -p -m -b -i myimage.png ``` This will convert a myimage png file to a map/pal/pic files with 16 colors,palette entry #0, 8x8 tiles, a blank tile, a map, no border, 16 colors output. From 32ddd8e1768e2528c1124a6db762105d8638416c Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sat, 9 Mar 2024 10:45:42 +0100 Subject: [PATCH 003/106] chore(*): update to new version --- LICENSE | 2 +- devkitsnes/readme.txt | 3 +++ pvsneslib/Makefile | 2 +- pvsneslib/pvsneslib_license.txt | 5 ++++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index af619cab..7602ceed 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2012-2023 alekmaul +Copyright (c) 2012-2024 alekmaul Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/devkitsnes/readme.txt b/devkitsnes/readme.txt index 2aef093e..b2a09fbe 100644 --- a/devkitsnes/readme.txt +++ b/devkitsnes/readme.txt @@ -29,6 +29,9 @@ SPECIAL THANKS CHANGE LOG ---------------------- +VERSION V4.2.1 (March,04,2024) +- See github changelog of the release + VERSION V4.2.0 (March,04,2024) - See github changelog of the release diff --git a/pvsneslib/Makefile b/pvsneslib/Makefile index d12923ca..55578d61 100644 --- a/pvsneslib/Makefile +++ b/pvsneslib/Makefile @@ -7,7 +7,7 @@ export TOPDIR := $(CURDIR) # create version number which will be used everywhere export PVSNESLIB_MAJOR := 4 export PVSNESLIB_MINOR := 2 -export PVSNESLIB_PATCH := 0 +export PVSNESLIB_PATCH := 1 export PVSNESLIB_VERSION := $(PVSNESLIB_MAJOR).$(PVSNESLIB_MINOR).$(PVSNESLIB_PATCH) # Directory with docs config and output (via doxygen) diff --git a/pvsneslib/pvsneslib_license.txt b/pvsneslib/pvsneslib_license.txt index 25493997..58d1d814 100644 --- a/pvsneslib/pvsneslib_license.txt +++ b/pvsneslib/pvsneslib_license.txt @@ -1,5 +1,5 @@ -------------------------------------------------------------------------- - Copyright (C) 2012-2023 + Copyright (C) 2012-2024 Alekmaul This software is provided 'as-is', without any express or implied @@ -21,6 +21,9 @@ -------------------------------------------------------------------------- CHANGE LOG -------------------------------------------------------------------------- +V4.2.1 (March,04,2024) +- See github changelog of the release + V4.2.0 (March,04,2024) - See github changelog of the release From 0e971e6d74338d95187ea59cdb365bdb9fe73f79 Mon Sep 17 00:00:00 2001 From: Carlos O'Connor Date: Mon, 18 Mar 2024 08:00:42 -0500 Subject: [PATCH 004/106] HiROM | FastROM support --- devkitsnes/snes_rules | 41 +++- pvsneslib/Makefile | 25 ++- pvsneslib/source/Makefile | 43 +++- pvsneslib/source/backgrounds.asm | 130 +++++------ pvsneslib/source/consoles.asm | 4 +- pvsneslib/source/crt0_snes.asm | 50 ++++- pvsneslib/source/dmas.asm | 212 +++++++++--------- pvsneslib/source/hdr.asm | 29 ++- pvsneslib/source/interrupts.asm | 9 +- pvsneslib/source/libc.asm | 35 ++- pvsneslib/source/libm.asm | 28 ++- pvsneslib/source/libtcc.asm | 13 +- pvsneslib/source/lzsss.asm | 40 ++-- pvsneslib/source/maps.asm | 10 +- pvsneslib/source/objects.asm | 10 +- pvsneslib/source/pads.asm | 108 ++++----- pvsneslib/source/scores.asm | 40 ++-- pvsneslib/source/snesmodwla.asm | 166 +++++++------- pvsneslib/source/sounds.asm | 3 +- pvsneslib/source/sprites.asm | 52 ++--- pvsneslib/source/videos.asm | 15 +- snes-examples/memory_mapping/Makefile | 27 +++ snes-examples/memory_mapping/data.asm | 11 + snes-examples/memory_mapping/hdr.asm | 87 +++++++ .../memory_mapping/pvsneslibfont.png | Bin 0 -> 1739 bytes .../memory_mapping/src/memory_mapping.c | 47 ++++ 26 files changed, 814 insertions(+), 421 deletions(-) create mode 100644 snes-examples/memory_mapping/Makefile create mode 100644 snes-examples/memory_mapping/data.asm create mode 100644 snes-examples/memory_mapping/hdr.asm create mode 100644 snes-examples/memory_mapping/pvsneslibfont.png create mode 100644 snes-examples/memory_mapping/src/memory_mapping.c diff --git a/devkitsnes/snes_rules b/devkitsnes/snes_rules index dcfa05a6..f6227f6f 100644 --- a/devkitsnes/snes_rules +++ b/devkitsnes/snes_rules @@ -3,8 +3,6 @@ ifeq ($(origin SRC), undefined) SRC := src endif -LIBDIRSOBJS := $(PVSNESLIB_HOME)/pvsneslib/lib - # to avoid some bugs if the PVSNESLIB_HOME is not well defined, we let a small check here ifeq ($(findstring \,$(PVSNESLIB_HOME)),\) $(error "PVSNESLIB_HOME environment variable is not defined correctly: the path must be in Unix style (on Windows operating system too!). For example, use /c/snesdev instead of c:\snesdev") @@ -18,6 +16,32 @@ DEBUG = 0 $(info The debug mode is NOT enabled, you can do it by executing "export PVSNESLIB_DEBUG=1") endif +ifeq ($(HIROM),1) +HIROM = 1 +$(info HiROM compilation is enabled) +ifeq ($(FASTROM),1) +LIBDIRSOBJS := $(PVSNESLIB_HOME)/pvsneslib/lib/HiROM_FastROM +else +LIBDIRSOBJS := $(PVSNESLIB_HOME)/pvsneslib/lib/HiROM_SlowROM +endif +else +HIROM = 0 +$(info LoROM compilation is enabled) +ifeq ($(FASTROM),1) +LIBDIRSOBJS := $(PVSNESLIB_HOME)/pvsneslib/lib/LoROM_FastROM +else +LIBDIRSOBJS := $(PVSNESLIB_HOME)/pvsneslib/lib/LoROM_SlowROM +endif +endif + +ifeq ($(FASTROM),1) +FASTROM = 1 +$(info FastROM compilation is enabled) +else +FASTROM = 0 +$(info SlowROM compilation is enabled) +endif + #--------------------------------------------------------------------------------- # on windows, linkfile can only manage path like E:\pvsneslib\lib\crt0_snes.obj # this one doesn't work /e/pvsneslib/lib/crt0_snes.obj @@ -85,7 +109,20 @@ export OFILES := $(BINFILES:.bin=.obj) $(CFILES:.c=.obj) $(SFILES:.asm=.obj) #--------------------------------------------------------------------------------- %.ps: %.c @echo Compiling to .ps ... $(notdir $<) +ifeq ($(HIROM),1) +ifeq ($(FASTROM),1) + $(CC) $(CFLAGS) -Wall -c $< -H -F -o $@ +else + $(CC) $(CFLAGS) -Wall -c $< -H -o $@ +endif +else +ifeq ($(FASTROM),1) + $(CC) $(CFLAGS) -Wall -c $< -F -o $@ +else $(CC) $(CFLAGS) -Wall -c $< -o $@ +endif +endif + ifeq ($(DEBUG),1) cp $@ $@.01.dbg endif diff --git a/pvsneslib/Makefile b/pvsneslib/Makefile index 55578d61..8bc40686 100644 --- a/pvsneslib/Makefile +++ b/pvsneslib/Makefile @@ -15,16 +15,35 @@ export PVSDOCSDIR := $(TOPDIR)/docs all: include/snes/libversion.h pvsneslibversion release docs +HIROM_VALUES := 0 1 +FASTROM_VALUES := 0 1 +KEEP_LIB := 0 + release: lib - $(MAKE) -C source all + @$(foreach HIROM, $(HIROM_VALUES), \ + $(foreach FASTROM, $(FASTROM_VALUES), \ + $(MAKE) HIROM=$(HIROM) FASTROM=$(FASTROM) build; \ + $(MAKE) KEEP_LIB=1 clean; \ + ) \ + ) + +build: + @$(MAKE) -C source all lib: @mkdir -p $@ + @mkdir -p $@/LoROM_SlowROM + @mkdir -p $@/LoROM_FastROM + @mkdir -p $@/HiROM_SlowROM + @mkdir -p $@/HiROM_FastROM clean: $(MAKE) -C source clean - @rm -rf $(PVSDOCSDIR)/html - @rm -f pvsneslib_version.txt + @if [ "$(KEEP_LIB)" -eq 0 ]; then \ + rm -rf $(PVSDOCSDIR)/html; \ + rm -f pvsneslib_version.txt; \ + rm -f lib/*ROM/*; \ + fi # Check if Doxygen is installed doxygenInstalled := $(shell command -v doxygen -q 2> /dev/null) diff --git a/pvsneslib/source/Makefile b/pvsneslib/source/Makefile index 7f47f310..0b8c37d0 100644 --- a/pvsneslib/source/Makefile +++ b/pvsneslib/source/Makefile @@ -23,11 +23,48 @@ export SNESOBJS = crt0_snes.obj libm.obj libtcc.obj libc.obj #--------------------------------------------------------------------------------- %.ps: %.c @echo $(notdir $<) + +ifeq ($(HIROM),1) +ifeq ($(FASTROM),1) + $(CC) $(CFLAGS) -Wall -c $< -H -F -o $@ +else + $(CC) $(CFLAGS) -Wall -c $< -H -o $@ +endif +else +ifeq ($(FASTROM),1) + $(CC) $(CFLAGS) -Wall -c $< -F -o $@ +else $(CC) $(CFLAGS) -Wall -c $< -o $@ +endif +endif sed -i 's/.include "hdr.asm"//' $@ -all: $(SNESOBJS) - @mv *.obj ../lib +reset_comp: + @echo "; HIROM / FASTROM definitions" > comp_defs.asm +ifeq ($(HIROM),1) + @echo ".DEFINE HIROM 1" >> comp_defs.asm +endif +ifeq ($(FASTROM),1) + @echo ".FASTROM" >> comp_defs.asm + @echo ".DEFINE FASTROM 1" >> comp_defs.asm +else + @echo ".SLOWROM" >> comp_defs.asm +endif + +all: reset_comp $(SNESOBJS) +ifeq ($(HIROM),1) +ifeq ($(FASTROM),1) + @mv *.obj ../lib/HiROM_FastROM +else + @mv *.obj ../lib/HiROM_SlowROM +endif +else +ifeq ($(FASTROM),1) + @mv *.obj ../lib/LoROM_FastROM +else + @mv *.obj ../lib/LoROM_SlowROM +endif +endif @rm -f *.ps #--------------------------------------------------------------------------------- @@ -41,4 +78,4 @@ clean: @echo clean ... @rm -f libc_c.ps libc_c.asm libm_c.asm @rm -f *.obj *.ps *.lst - @rm -f ../lib/* + @rm -f comp_defs.asm \ No newline at end of file diff --git a/pvsneslib/source/backgrounds.asm b/pvsneslib/source/backgrounds.asm index 9c2dd7b8..0c7e3faf 100644 --- a/pvsneslib/source/backgrounds.asm +++ b/pvsneslib/source/backgrounds.asm @@ -1,7 +1,7 @@ ;--------------------------------------------------------------------------------- ; ; Copyright (C) 2013-2021 -; Alekmaul +; Alekmaul ; ; This software is provided 'as-is', without any express or implied ; warranty. In no event will the authors be held liable for any @@ -49,7 +49,8 @@ .EQU SC_32x32 (0 << 0) -.RAMSECTION ".reg_bkgrd7e" BANK $7E +.BASE $00 +.RAMSECTION ".reg_bkgrd7e" BANK $7E SLOT RAMSLOT_0 bg0gfxaddr DSB 2 bg1gfxaddr DSB 2 @@ -60,6 +61,7 @@ bkgrd_val1 DSB 2 ; save value #1 .ENDS +.BASE BASE_0 .SECTION ".backgrounds0_text" SUPERFREE ;--------------------------------------------------------------------------- @@ -67,12 +69,12 @@ bkgrd_val1 DSB 2 ; save value #1 bgSetScroll: php phb - + sep #$20 lda #$0 pha plb ; change bank address to 0 - + lda 6,s ; bgNumber rep #$20 and #$0003 ; do not exceed bg @@ -84,26 +86,26 @@ bgSetScroll: sep #$20 sta REG_BGxHOFS,y rep #$20 - xba + xba sep #$20 sta REG_BGxHOFS,y - rep #$20 + rep #$20 lda 11,s ; y scrolling offset sep #$20 sta REG_BGyHOFS,y rep #$20 - xba + xba sep #$20 sta REG_BGyHOFS, y rep #$20 ply - + plb plp rtl - + .ENDS .SECTION ".backgrounds1_text" SUPERFREE @@ -115,7 +117,7 @@ bgSetEnable: sep #$20 lda 5,s - + rep #$20 and #$00ff sep #$20 @@ -125,15 +127,15 @@ bgSetEnable: beq + - asl a dey - bne - - + bne - + + sta bkgrd_val1 ; videoMode |= (1 << bgNumber); lda videoMode ora bkgrd_val1 sta videoMode - + sta.l REG_TM ; REG_TM = videoMode; - + plp rtl @@ -144,7 +146,7 @@ bgSetDisable: sep #$20 lda 5,s - + rep #$20 and #$00ff sta bkgrd_val1 @@ -154,16 +156,16 @@ bgSetDisable: beq + - asl a dey - bne - - + bne - + + eor #$FF sta bkgrd_val1 ; videoMode &= ~(1 << bgNumber); lda videoMode and bkgrd_val1 sta videoMode - + sta.l REG_TM ; REG_TM = videoMode; - + plp rtl @@ -178,7 +180,7 @@ bgSetEnableSub: sep #$20 lda 5,s - + rep #$20 and #$00ff sta bkgrd_val1 @@ -188,15 +190,15 @@ bgSetEnableSub: beq + - asl a dey - bne - - + bne - + + sta bkgrd_val1 ; videoModeSub |= (1 << bgNumber); lda videoModeSub ora bkgrd_val1 sta videoModeSub - + sta.l REG_TS ; REG_TS = videoModeSub; - + plp rtl @@ -207,7 +209,7 @@ bgSetDisableSub: sep #$20 lda 5,s - + rep #$20 and #$00ff sta bkgrd_val1 @@ -217,19 +219,19 @@ bgSetDisableSub: beq + - asl a dey - bne - - + bne - + + eor #$FF sta bkgrd_val1 ; videoModeSub &= ~(1 << bgNumber); lda videoModeSub and bkgrd_val1 sta videoModeSub - + sta.l REG_TS ; REG_TS = videoModeSub; - + plp rtl - + .ENDS @@ -255,7 +257,7 @@ bgSetWindowsRegions: lda #$11 sta.l REG_TMW - + plp rtl @@ -268,7 +270,7 @@ bgSetWindowsRegions: bgSetGfxPtr: php phx - + sep #$20 lda 7,s ; get bgNumber rep #$20 @@ -277,10 +279,10 @@ bgSetGfxPtr: tax lda 8,s sta bg0gfxaddr,x ; store address - + txa ; Change address regarde background number cmp #4 - bcs + ; if bg>=2 + bcs + ; if bg>=2 lda bg0gfxaddr ; REG_BG12NBA = (bgState[1].gfxaddr >> 8 ) | (bgState[0].gfxaddr >> 12); ldx #12 - ror a @@ -319,12 +321,12 @@ _bSGP1: bgSetMapPtr: php phx - + sep #$20 ; mapadr = ((address >> 8) & 0xfc) | (mapSize & 0x03); lda 10,s ; get mapsize and #$0003 sta bkgrd_val1 - + rep #$20 lda 8,s ; get address ldx #8 @@ -335,7 +337,7 @@ bgSetMapPtr: sep #$20 ora bkgrd_val1 sta bkgrd_val1 - + lda 7,s rep #$20 and #$0003 @@ -343,11 +345,11 @@ bgSetMapPtr: sep #$20 lda bkgrd_val1 sta.l BG1SC_ADDR,x - + plx plp rtl - + .ENDS .SECTION ".backgrounds5_text" SUPERFREE @@ -357,7 +359,7 @@ bgSetMapPtr: ;5 6-9 10-13 14 15-16 17-18 19-20 21-22 bgInitTileSet: php - + ; If mode 0, compute palette entry with separate subpalettes in entries 0-31, 32-63, 64-95, and 96-127 rep #$20 lda 19,s ; get colorMode @@ -394,7 +396,7 @@ bgInitTileSet: dex bne - + sta bkgrd_val1 - + _bITS1: sep #$20 lda #0 @@ -407,7 +409,7 @@ _bITS1: tas wai - lda 15,s ; get tilesize + lda 15,s ; get tilesize pha lda 23,s ; get address (21+2) pha @@ -420,8 +422,8 @@ _bITS1: clc adc #8 tas - - lda 17,s ; get paletteSize + + lda 17,s ; get paletteSize pha lda bkgrd_val1 pha @@ -435,7 +437,7 @@ _bITS1: adc #8 tas - lda 21,s ; get address + lda 21,s ; get address pha sep #$20 lda 7,s ; get bgNumber (5+2) @@ -459,7 +461,7 @@ _bITS1: ;5 6-9 10-13 14 15-16 17-18 19-20 bgInitTileSetLz: php - + ; If mode 0, compute palette entry with separate subpalettes in entries 0-31, 32-63, 64-95, and 96-127 rep #$20 lda 17,s ; get colorMode @@ -496,7 +498,7 @@ bgInitTileSetLz: dex bne - + sta bkgrd_val1 - + _bITS1: sep #$20 lda #0 @@ -509,7 +511,7 @@ _bITS1: tas wai - lda 19,s ; get address + lda 19,s ; get address pha lda 10,s ; get tileSource bank address (8+2) pha @@ -520,8 +522,8 @@ _bITS1: clc adc #6 tas - - lda 15,s ; get paletteSize + + lda 15,s ; get paletteSize pha lda bkgrd_val1 pha @@ -535,7 +537,7 @@ _bITS1: adc #8 tas - lda 19,s ; get address + lda 19,s ; get address pha sep #$20 lda 7,s ; get bgNumber (5+2) @@ -559,11 +561,11 @@ _bITS1: ; 5 6-9 10-11 12 13-14 bgInitMapSet: php - + wai rep #$20 - lda 10,s ; get mapSize + lda 10,s ; get mapSize pha lda 15,s ; get address (13+2) pha @@ -581,7 +583,7 @@ bgInitMapSet: lda 5,s cmp #$ff beq + ; do only if number is not 0xFF - + lda 12,s ; get sizeMode pha rep #$20 @@ -609,7 +611,7 @@ bgInitMapSet: ; 5 6-9 10-11 12-13 bgInitTileSetData: php - + sep #$20 lda #0 pha @@ -622,7 +624,7 @@ bgInitTileSetData: wai rep #$20 - lda 10,s ; get tileSize + lda 10,s ; get tileSize pha lda 14,s ; get address (12+2) pha @@ -635,14 +637,14 @@ bgInitTileSetData: clc adc #8 tas - + sep #$20 lda 5,s cmp #$ff beq + ; do only if number is not 0xFF - + rep #$20 - lda 12,s ; get address + lda 12,s ; get address pha sep #$20 lda 7,s ; get bgNumber (5+2) @@ -666,7 +668,7 @@ bgInitTileSetData: ; 5-8 9-12 13-16 17-18 19-20 bgInitMapTileSet7: php - + sep #$20 lda #0 pha @@ -697,7 +699,7 @@ bgInitMapTileSet7: clc adc #11 tas - + sep #$20 lda #SC_32x32 pha @@ -713,7 +715,7 @@ bgInitMapTileSet7: clc adc #4 tas - + lda #$1900 ; dmaCopyVram7(tileSource, address, tileSize, VRAM_INCHIGH | VRAM_ADRTR_0B | VRAM_ADRSTINC_1,0x1900); pha sep #$20 @@ -733,7 +735,7 @@ bgInitMapTileSet7: clc adc #11 tas - + lda #256*2 ; dmaCopyCGram(tilePalette, 0, 256*2); pha lda #0 @@ -747,7 +749,7 @@ bgInitMapTileSet7: clc adc #8 tas - + lda 19,s ; get address pha sep #$20 diff --git a/pvsneslib/source/consoles.asm b/pvsneslib/source/consoles.asm index 8ac6fb7b..294493f9 100644 --- a/pvsneslib/source/consoles.asm +++ b/pvsneslib/source/consoles.asm @@ -38,7 +38,8 @@ .DEFINE TXT_VRAMBGADR $0800 .DEFINE TXT_VRAMOFFSET $0000 -.RAMSECTION ".reg_cons7e" BANK $7E +.BASE $00 +.RAMSECTION ".reg_cons7e" BANK $7E SLOT RAMSLOT_0 snes_vblank_count DW ; to count number of vblank @@ -64,6 +65,7 @@ snes_rand_seed1: DSB 2 snes_rand_seed2: DSB 2 .ENDS +.BASE BASE_0 .SECTION ".consoles0_text" SUPERFREE ;--------------------------------------------------------------------------- diff --git a/pvsneslib/source/crt0_snes.asm b/pvsneslib/source/crt0_snes.asm index fd4f3420..91e8013a 100644 --- a/pvsneslib/source/crt0_snes.asm +++ b/pvsneslib/source/crt0_snes.asm @@ -38,13 +38,22 @@ tcc__regs_irq dsb 48 .ENDS -.SECTION "glob.data" SUPERFREE KEEP +.BANK 0 ; Defines the ROM bank and the slot it is inserted in memory. + +.ifdef FASTROM +.BASE $80 +.endif + +.DEFINE ORG_0 0 +.ifdef HIROM +.REDEFINE ORG_0 $8000 +.endif + +.SECTION "glob.data" SEMIFREE ORG ORG_0 KEEP .ENDS -.BANK 0 ; Defines the ROM bank and the slot it is inserted in memory. -.ORG 0 ; .ORG 0 is really $8000, because the slot starts at $8000 -.SECTION "EmptyVectors" SEMIFREE +.SECTION "EmptyVectors" SEMIFREE ORG ORG_0 EmptyHandler: rti @@ -57,8 +66,7 @@ EmptyNMI: .EMPTYFILL $00 ; fill unused areas with $00, opcode for BRK. ; BRK will crash the snes if executed. -.BANK 0 -.SECTION "Snes_Init" SEMIFREE +.SECTION "Snes_Init" SEMIFREE ORG ORG_0 tcc__snesinit: rep #$10 ; X/Y 16 bit sep #$20 ; A is 8 bit @@ -188,16 +196,31 @@ tcc__snesinit: ldx.w #$4202 - stz 0,x inx - cpx.w #$420e +.ifdef FASTROM + cpx.w #$420d bne - + sep #$20 ; A 16 bit + lda #$01 + sta $420d ; FastROM enabled +.else + cpx.w #$420e + bne - +.endif ;cli ; Enable interrupts rts -.ENDS +.ENDS ; Needed to satisfy interrupt definition in "Header.inc". +.SECTION ".vblank" SEMIFREE ORG ORG_0 + VBlank: +.ifdef FASTROM + jml FVBlank + +FVBlank: +.endif rep #$30 phb phd @@ -222,19 +245,24 @@ VBlank: plb RTI -.BANK 0 -.SECTION ".start" +.ENDS + +.SECTION ".start" SEMIFREE ORG ORG_0 .accu 16 .index 16 .16bit - tcc__start: ; Initialize the SNES. sei ; Disabled interrupts clc ; clear carry to switch to native mode xce ; Xchange carry & emulation bit. native mode +.ifdef FASTROM + jml fast_start + +fast_start: +.endif rep #$18 ; Binary mode (decimal mode off), X/Y 16 bit ldx #$1FFF ; set stack to $1FFF txs diff --git a/pvsneslib/source/dmas.asm b/pvsneslib/source/dmas.asm index 5fede348..eca77182 100644 --- a/pvsneslib/source/dmas.asm +++ b/pvsneslib/source/dmas.asm @@ -1,7 +1,7 @@ ;--------------------------------------------------------------------------------- ; ; Copyright (C) 2013-2021 -; Alekmaul +; Alekmaul ; ; This software is provided 'as-is', without any express or implied ; warranty. In no event will the authors be held liable for any @@ -25,12 +25,13 @@ .EQU REG_RDNMI $4210 .EQU REG_HDMAEN $420C -.RAMSECTION ".reg_dma7e" BANK $7E +.BASE $00 +.RAMSECTION ".reg_dma7e" BANK $7E SLOT RAMSLOT_0 HDMATable16 DSB 224*3+1 ; enough lines for big hdma features hdma_val1 DSB 2 ; save value #1 - + hdmacirc_x DW hdmacirc_y DW hdmacirc_ysav DW @@ -44,7 +45,8 @@ hdmacirc_err DW .index 16 .16bit -.SECTION ".dmas0_text" SUPERFREE +.BASE BASE_0 +.SECTION ".dmas0_text" SUPERFREE ;--------------------------------------------------------------------------- ; void dmaCopyCGram(u8 * source, u16 address, u16 size); @@ -53,7 +55,7 @@ dmaCopyCGram: ; jsr.w _wait_nmid rep #$20 - + lda 11,s ; numBytes sta.l $4305 lda 5,s ; src (lower 16 bits) @@ -69,7 +71,7 @@ dmaCopyCGram: sta.l $4301 lda #1 sta.l $420b - + plp rtl @@ -84,12 +86,12 @@ dmaCopyVram: ; jsr.w _wait_nmid rep #$20 - lda 9,s + lda 9,s sta.l $2116 ; address for VRAM write(or read) lda 11,s sta.l $4305 ; number of bytes to be copied - lda 5,s + lda 5,s sta.l $4302 ; data offset in memory sep #$20 ; 8bit A @@ -119,13 +121,13 @@ dmaCopySpr32Vram: ; jsr.w _wait_nmid rep #$20 - lda 9,s + lda 9,s sta.l $2116 ; address for VRAM write(or read) ; lda 11,s lda #$80 sta.l $4305 ; number of bytes to be copied - lda 5,s + lda 5,s sta.l $4302 ; data offset in memory sep #$20 ; 8bit A @@ -160,10 +162,10 @@ dmaCopySpr32Vram: rep #$20 clc lda #$200 - adc 9,s + adc 9,s sta.l $2116 ; address for VRAM write(or read) lda #$400 - adc 5,s + adc 5,s sta.l $4302 ; data offset in memory lda #$80 sta.l $4305 ; number of bytes to be copied @@ -175,10 +177,10 @@ dmaCopySpr32Vram: rep #$20 clc lda #$300 - adc 9,s + adc 9,s sta.l $2116 ; address for VRAM write(or read) lda #$600 - adc 5,s + adc 5,s sta.l $4302 ; data offset in memory lda #$80 sta.l $4305 ; number of bytes to be copied @@ -200,13 +202,13 @@ dmaCopySpr16Vram: ; jsr.w _wait_nmid rep #$20 - lda 9,s + lda 9,s sta.l $2116 ; address for VRAM write(or read) ; lda 11,s lda #$40 sta.l $4305 ; number of bytes to be copied - lda 5,s + lda 5,s sta.l $4302 ; data offset in memory sep #$20 ; 8bit A @@ -226,10 +228,10 @@ dmaCopySpr16Vram: rep #$20 clc lda #$100 - adc 9,s + adc 9,s sta.l $2116 ; address for VRAM write(or read) lda #$200 - adc 5,s + adc 5,s sta.l $4302 ; data offset in memory lda #$40 sta.l $4305 ; number of bytes to be copied @@ -251,12 +253,12 @@ dmaFillVram: ; jsr.w _wait_nmid rep #$20 - lda 9,s + lda 9,s sta.l $2116 ; address for VRAM write(or read) lda 11,s sta.l $4305 ; number of bytes to be copied - lda 5,s + lda 5,s sta.l $4302 ; data offset in memory sep #$20 ; 8bit A @@ -288,7 +290,7 @@ dmaClearVram: lda #$80 sta.l $2115 ;Set VRAM port to word access - rep #$20 + rep #$20 lda #$1809 sta.l $4300 ;Set DMA mode to fixed source, WORD to $2118/9 lda #$0000 @@ -349,8 +351,8 @@ dmaCopyOAram: lda #0 sta.l $4300 lda #$04 - sta.l $4301 ; DMA channel 0 Bus B addr $2104 (OAM write) - + sta.l $4301 ; DMA channel 0 Bus B addr $2104 (OAM write) + lda #1 sta.l $420b @@ -373,9 +375,9 @@ dmaCopyVram7: sta.l $2115 ; block size transfer ($2115) rep #$20 - lda 9,s ; address in VRam for read or write ($2116) + lda 9,s ; address in VRam for read or write ($2116) sta.l $2116 - + lda 11,s ; numBytes sta.l $4305 lda 5,s ; src (lower 16 bits) @@ -384,19 +386,19 @@ dmaCopyVram7: lda 14,s and #$00ff sep #$20 - sta.l $4300 ; (dmacontrol & 255) - + sta.l $4300 ; (dmacontrol & 255) + rep #$20 lda 14,s xba and #$00ff sep #$20 sta.l $4301 ; (dmacontrol>>8) - + sep #$20 lda 7,s ; src bank sta.l $4304 - + lda #1 sta.l $420b @@ -412,28 +414,28 @@ dmaCopyVram7: setModeHdmaGradient: php phx - + ; jsr.w _wait_nmid ldx #$0000 - + sep #$20 lda 8,s and #$f ; maxLevels & 15 sta hdma_val1 - + _sMHG1: sep #$20 lda #14 ; because we have 224 lines and 16 steps 224/16 -> 14 sta HDMATable16,x - + rep #$20 ; mL - (i / (32/(mL+1))); phx - lda.w #32 - tax - sep #$20 + lda.w #32 + tax + sep #$20 lda hdma_val1 rep #$20 - ina + ina jsl tcc__div ; 1) 32/(mL+1) -> x=32, a=mL+1 plx lda.b tcc__r9 @@ -450,10 +452,10 @@ _sMHG1: txa cmp #32 bne _sMHG1 - + lda #0 ; finish the dma table with 0 sta HDMATable16,x - + sta.l $4330 ; 0x00 Meaning write once sta.l $4331 ; 0x00 Screen display register -> so we control brightness @@ -464,10 +466,10 @@ _sMHG1: sep #$20 lda #:HDMATable16 ; src bank sta.l $4334 - + lda #8 ; Enable HDMA channel 3 sta.l REG_HDMAEN - + plx plp rtl @@ -511,7 +513,7 @@ _Lvl1Bright: .db $03,$01 .db $03,$00 .db $00 - + setModeHdmaShadeUpDown: php @@ -527,10 +529,10 @@ setModeHdmaShadeUpDown: sep #$20 lda #:_Lvl1Bright ; src bank sta.l $4334 - + lda #8 ; Enable HDMA channel 3 sta.l REG_HDMAEN - + plp rtl @@ -551,7 +553,7 @@ setModeHdmaShading: sep #$20 lda 5,s ; mode=0, we stop shading beq + - + sep #$20 lda #0 sta.l $4300 ; 0x00 Meaning write once @@ -601,33 +603,33 @@ setModeHdmaShading: _bgscridx: .db $0D,$0F,$11 ; for horizontal scrolling registers 210d, 210f & 2111 (bg0..2) - + //--------------------------------------------------------------------------------- ; void setParallaxScrolling(u8 bgrnd) setParallaxScrolling: php phx - + sep #$20 lda #$02 ; direct mode sta.l $4330 ; 1 register, write twice (mode 2) - + lda 7,s ; get background number (5+2) rep #$20 and #$00ff tax sep #$20 - lda.l _bgscridx,x ; bgx_scroll_x horizontal scroll + lda.l _bgscridx,x ; bgx_scroll_x horizontal scroll sta.l $4331 ; destination - + lda #:HDMATable16 ; src bank sta.l $4334 - rep #$20 + rep #$20 lda #HDMATable16.w ; Address of HDMA table, get high and low byte sta.l $4332 ; 4332 = Low-Byte of table, 4333 = High-Byte of table - - sep #$20 + + sep #$20 lda #8 ; Enable HDMA channel 3 sta.l REG_HDMAEN @@ -646,20 +648,20 @@ setModeHdmaReset: lda 5,s sta.l REG_HDMAEN - plp + plp rtl ; void setModeHdmaWindowReset(u8 channels) setModeHdmaWindowReset: php - + sep #$20 lda 5,s sta.l REG_HDMAEN lda #$00 sta.l REG_TMW - plp + plp rtl .ENDS @@ -694,7 +696,7 @@ setModeHdmaColor: plp rtl - + ;.ENDS ;.SECTION ".dmas14_text" SUPERFREE @@ -711,7 +713,7 @@ setModeHdmaWaves: pha plb - rep #$20 + rep #$20 ldx #0 _smhw01: @@ -722,7 +724,7 @@ _smhw01: txa cmp #34 bne _smhw01 - + sep #$20 lda #$0 pha @@ -744,7 +746,7 @@ _smhw01: sta $4364 ; address lda #$7e sta.l $4367 ; indirect address bank - + lda #$40 ; channel 6 sta.l REG_HDMAEN @@ -756,7 +758,7 @@ _smhw01: ; void setModeHdmaWavesMove(void) setModeHdmaWavesMove: php - phb + phb sep #$20 lda #$7e @@ -764,13 +766,13 @@ setModeHdmaWavesMove: plb rep #$20 - lda snes_vblank_count ; only does this every 4th frame + lda snes_vblank_count ; only does this every 4th frame and #$0003 bne _smhwm0 lda HDMATable16 sta hdma_val1 - + lda HDMATable16+2 sta HDMATable16 lda HDMATable16+4 @@ -801,13 +803,13 @@ setModeHdmaWavesMove: sta HDMATable16+26 lda HDMATable16+30 sta HDMATable16+28 - + lda hdma_val1 sta HDMATable16+30 _smhwm0: - plb + plb plp - rtl + rtl waveHTable: ; indirect table for wave effect .byte 8 @@ -869,24 +871,24 @@ waveHTable: ; indirect table f .byte 0 _waveTable: - .word $00 ; | - .word $03 ; | - .word $06 ; | - .word $07 ; | - .word $08 ; | - .word $07 ; | - .word $06 ; | - .word $03 ; | - .word $00 ; | - .word -$03 ; | - .word -$06 ; | - .word -$07 ; | - .word -$08 ; | - .word -$07 ; | - .word -$06 ; | - .word -$03 ; | - .word $00 ;/ - + .word $00 ; | + .word $03 ; | + .word $06 ; | + .word $07 ; | + .word $08 ; | + .word $07 ; | + .word $06 ; | + .word $03 ; | + .word $00 ; | + .word -$03 ; | + .word -$06 ; | + .word -$07 ; | + .word -$08 ; | + .word -$07 ; | + .word -$06 ; | + .word -$03 ; | + .word $00 ;/ + .ENDS .SECTION ".dmas14_text" SUPERFREE @@ -913,30 +915,30 @@ setModeHdmaWindow: lda 9,s ; got all the flags to mask effect (inside, outside on BG1..2) sta REG_W12SEL bra ++ -+: ++: lda 9,s ; got all the flags to mask effect (inside, outside on BG3..4) sta REG_W34SEL -++: lda 9,s ; todo : find a way to manage easily objects -> currently, it works only for BG1 +++: lda 9,s ; todo : find a way to manage easily objects -> currently, it works only for BG1 sta REG_WOBJSEL - + stz $4340 ; 1 register, write once lda #$26 ; 2126 Window 1 Left Position (X1) sta $4341 ; destination lda 12,s ; bank address of left table - sta $4344 - + sta $4344 + stz $4350 ; 1 register, write once lda #$27 ; 2127 Window 1 Right Position (X2) - sta $4351 + sta $4351 lda 16,s ; bank address of right table - sta $4354 + sta $4354 rep #$20 lda 10,s ; low address of left table sta $4342 ; low address of right table - lda 14,s - sta $4352 + lda 14,s + sta $4352 sep #$20 lda #$30 ; channel 4 & 5 00110000 @@ -945,7 +947,7 @@ setModeHdmaWindow: plx plb plp - rtl + rtl .ENDS @@ -962,13 +964,13 @@ calc_circle_hdma: lda #$7E pha plb - + rep #$20 lda 12,s ; get radius - bne + + bne + brl _cchend ; radius =0, go out +: sta hdmacirc_x ; x = rc - + stz hdmacirc_y ; y = 0 stz hdmacirc_err ; error=0 (16 bits) @@ -983,7 +985,7 @@ calc_circle_hdma: sta tcc__r0h lda 20,s sta tcc__r1h - + rep #$20 lda 10,s ; 1st, store nb skipped lines -> get y0 sec @@ -1055,7 +1057,7 @@ _cchiterskip1: sta hdmacirc_ysav _cchloop: - rep #$20 ; pixel (x0-y,y0-x) x->rc..0 y=0..n + rep #$20 ; pixel (x0-y,y0-x) x->rc..0 y=0..n lda 10,s ; get y0 sec sbc hdmacirc_x ; y0-x @@ -1081,7 +1083,7 @@ _cchloop: sta [tcc__r1], y phy tya - + rep #$20 @@ -1093,7 +1095,7 @@ _cchloop: ina sta hdmacirc_err inc hdmacirc_y ; y++ - + sec sbc hdmacirc_x ; error - x dea @@ -1146,7 +1148,7 @@ _cchskip1: ; sbc hdmacirc_tmp ; sep #$20 ; sta [tcc__r1], y - + ; rep #$20 ; lda hdmacirc_err ; error += 1 + 2*y ; clc @@ -1156,7 +1158,7 @@ _cchskip1: ; ina ; sta hdmacirc_err ; inc hdmacirc_y ; y++ - + ; sec ; sbc hdmacirc_x ; error - x ; dea @@ -1193,12 +1195,12 @@ _cchend: sta [tcc__r0], y lda #$0 sta [tcc__r1], y - iny + iny lda #0 sta [tcc__r0], y sta [tcc__r1], y rep #$20 - lda hdmacirc_ysav ; + lda hdmacirc_ysav ; ora #$80 ; need to put bit 7 sta hdmacirc_ysav lda #$2 diff --git a/pvsneslib/source/hdr.asm b/pvsneslib/source/hdr.asm index b549aeeb..5c77f45c 100644 --- a/pvsneslib/source/hdr.asm +++ b/pvsneslib/source/hdr.asm @@ -20,18 +20,37 @@ ; 3. This notice may not be removed or altered from any source ; distribution. ; -; ==LoRom== ; We'll get to HiRom some other time. ;--------------------------------------------------------------------------------- +.include "comp_defs.asm" ; Contains definitions for HiROM and FastROM + +.ifdef HIROM ; ==HiRom== + +.MEMORYMAP ; Begin describing the system architecture. + SLOTSIZE $10000 ; The slot is $10000 bytes in size. More details on slots later. + DEFAULTSLOT 0 ; There's only 1 slot in SNES, there are more in other consoles. + SLOT 0 $0000 ; Defines Slot 0's starting address. + SLOT 1 $0 $2000 ; Used for low RAM allocation + SLOT 2 $2000 $E000 ; Used for RAM allocation + SLOT 3 $0 $10000 ; Used for global RAM allocation + SLOT 4 $6000 ; Used for SRAM storage. +.ENDME ; End MemoryMap definition + +.ROMBANKSIZE $10000 ; Every ROM bank is 64 KBytes in size + +.else ; ==LoRom== + .MEMORYMAP ; Begin describing the system architecture. SLOTSIZE $8000 ; The slot is $8000 bytes in size. More details on slots later. DEFAULTSLOT 0 ; There's only 1 slot in SNES, there are more in other consoles. SLOT 0 $8000 ; Defines Slot 0's starting address. - SLOT 1 $0 $2000 - SLOT 2 $2000 $E000 - SLOT 3 $0 $10000 + SLOT 1 $0 $2000 ; Used for low RAM allocation + SLOT 2 $2000 $E000 ; Used for RAM allocation + SLOT 3 $0 $10000 ; Used for global RAM allocation and data storage .ENDME ; End MemoryMap definition .ROMBANKSIZE $8000 ; Every ROM bank is 32 KBytes in size -.ROMBANKS 8 ; 2 Mbits - Tell WLA we want to use 8 ROM Banks +.endif + +.ROMBANKS 2 ; Tell WLA we want to use 2 ROM Banks \ No newline at end of file diff --git a/pvsneslib/source/interrupts.asm b/pvsneslib/source/interrupts.asm index d9811a92..da78dd83 100644 --- a/pvsneslib/source/interrupts.asm +++ b/pvsneslib/source/interrupts.asm @@ -22,13 +22,14 @@ ; ;--------------------------------------------------------------------------------- -.SECTION ".interrupts0_text" SUPERFREE +.BASE BASE_0 +.SECTION ".interrupts0_text" SUPERFREE ;--------------------------------------------------------------------------- WaitForVBlank: wai rtl - + ; old version still here for memory purpose ; pha ; php @@ -58,8 +59,8 @@ WaitNVBlank: - wai dea bne - - + plp rtl - + .ENDS diff --git a/pvsneslib/source/libc.asm b/pvsneslib/source/libc.asm index 27972960..ea8613d1 100644 --- a/pvsneslib/source/libc.asm +++ b/pvsneslib/source/libc.asm @@ -1,6 +1,17 @@ .INCLUDE "hdr.asm" -.SECTION ".libc_mem" SUPERFREE +.ifdef FASTROM +.BASE $80 +.endif + +.BANK 1 + +.DEFINE ORG_0 0 +.ifdef HIROM +.REDEFINE ORG_0 $8000 +.endif + +.SECTION ".libc_mem" SEMIFREE ORG ORG_0 .accu 16 .index 16 @@ -344,7 +355,7 @@ strrchr: .ENDS -.SECTION ".libc_misc" +.SECTION ".libc_misc" SEMIFREE ORG ORG_0 .accu 16 .index 16 @@ -404,7 +415,7 @@ longjmp: .ENDS -.SECTION ".libc_cstd" +.SECTION ".libc_cstd" SEMIFREE ORG ORG_0 .accu 16 .index 16 @@ -441,6 +452,24 @@ exitl4: .include "libc_c.asm" +.DEFINE RAMSLOT_0 0 +.ifdef HIROM +.REDEFINE RAMSLOT_0 2 +.endif + +.DEFINE BASE_0 $00 +.ifdef FASTROM +.ifdef HIROM +.REDEFINE BASE_0 $C0 +.else +.REDEFINE BASE_0 $80 +.endif +.else +.ifdef HIROM +.REDEFINE BASE_0 $40 +.endif +.endif + .include "backgrounds.asm" .include "consoles.asm" .include "dmas.asm" diff --git a/pvsneslib/source/libm.asm b/pvsneslib/source/libm.asm index 8120c086..b1809f2a 100644 --- a/pvsneslib/source/libm.asm +++ b/pvsneslib/source/libm.asm @@ -4,6 +4,7 @@ .index 8 .8bit +.BASE $00 .RAMSECTION ".fp_libm" BANK 0 SLOT 1 PRIORITY 2 tcc__SIGN dsb 1 tcc__f1 dsb 0 @@ -19,7 +20,18 @@ tcc__E DSB 4 ;.define OVLOC $3f5 -.SECTION ".libm" SUPERFREE +.ifdef FASTROM +.BASE $80 +.endif + +.BANK 0 + +.DEFINE ORG_0 0 +.ifdef HIROM +.REDEFINE ORG_0 $8000 +.endif + +.SECTION ".libm" SEMIFREE ORG ORG_0 .accu 16 .index 16 @@ -124,7 +136,7 @@ _lltof_discard_low: .accu 8 .index 8 -_lltof_discard_high: +_lltof_discard_high: ; discard highest byte tya sec @@ -139,7 +151,7 @@ _lltof_discard_high: xba sta.b tcc__M1 sep #$20 - + rts @@ -238,23 +250,23 @@ tcc__llfloat: ldy.w #$8E + 16 ;INIT EXP1 TO 14 + 16, lda.b tcc__r9 xba sta.b tcc__M1 + 1 - + ++ sep #$30 sty.b tcc__X1 jsr _norm rep #$30 - rtl + rtl .accu 16 .index 16 - + tcc__float: sep #$30 stz.b tcc__M1 + 2 ; value loaded is 16 bits, clear low byte of ; 24 bit mantissa jsr float rep #$30 - rtl + rtl tcc__ufloat: sep #$30 @@ -269,7 +281,7 @@ tcc__ufloat: jsr _norm1 rep #$30 rtl - + tcc__fcmp: sep #$30 jsr fsub diff --git a/pvsneslib/source/libtcc.asm b/pvsneslib/source/libtcc.asm index c33d3259..00e057dc 100644 --- a/pvsneslib/source/libtcc.asm +++ b/pvsneslib/source/libtcc.asm @@ -1,6 +1,17 @@ .INCLUDE "hdr.asm" -.SECTION ".libtcc_internal" +.ifdef FASTROM +.BASE $80 +.endif + +.BANK 1 + +.DEFINE ORG_0 0 +.ifdef HIROM +.REDEFINE ORG_0 $8000 +.endif + +.SECTION ".libc_mem" SEMIFREE ORG ORG_0 .accu 16 .index 16 diff --git a/pvsneslib/source/lzsss.asm b/pvsneslib/source/lzsss.asm index 041769ae..194b4145 100644 --- a/pvsneslib/source/lzsss.asm +++ b/pvsneslib/source/lzsss.asm @@ -1,7 +1,7 @@ ;--------------------------------------------------------------------------------- ; ; Copyright (C) 2014-2022 -; Alekmaul +; Alekmaul ; ; This software is provided 'as-is', without any express or implied ; warranty. In no event will the authors be held liable for any @@ -30,7 +30,8 @@ .EQU REG_VMDATALREAD $2139 ; VRAM Data Read Low 1B/R .EQU REG_VMDATAHREAD $213A ; VRAM Data Read High 1B/R -.RAMSECTION ".reg_lzss7e" BANK $7e +.BASE $00 +.RAMSECTION ".reg_lzss7e" BANK $7E SLOT RAMSLOT_0 m0 DW m4 DW @@ -39,6 +40,7 @@ m6 DW .ENDS +.BASE BASE_0 .SECTION ".lz77_text" superfree .accu 16 @@ -56,7 +58,7 @@ m6 DW ;--------------------------------------------------------------------------- ; void LzssDecodeVram(u8 * source, u16 address); -; 10-13 14-15 +; 10-13 14-15 LzssDecodeVram: php phb @@ -75,7 +77,7 @@ LzssDecodeVram: sep #$20 lda 12,s sta tcc__r0h - + rep #$20 lda 14,s asl a ; because adr is x2 @@ -83,13 +85,13 @@ LzssDecodeVram: sep #$20 lda #$00 ; setup VRAM access (increment on readlow) - sta.l REG_VMAIN + sta.l REG_VMAIN ldy #0 ; y = 0 (source index) lda [tcc__r0] ; test compression type - and #$F0 - cmp #$10 ; 1x = LZ77 - beq @LZ77source + and #$F0 + cmp #$10 ; 1x = LZ77 + beq @LZ77source @ddv_exit: ply @@ -101,24 +103,24 @@ LzssDecodeVram: @LZ77source: ;========================================================================= iny ; x = byte 1,2 (data length) - rep #$20 + rep #$20 lda [tcc__r0], y - tax - sep #$20 + tax + sep #$20 iny iny iny @LZ77_DecompressLoop: lda [tcc__r0], y ; m5 = cflags - iny - sta m5 + iny + sta m5 lda #8 ; m6 = bit counter - sta m6 + sta m6 @next_bit: asl m5 ; test bit - bcs @lz_byte + bcs @lz_byte @raw_byte: rep #$20 ; setup vram address (to target) @@ -126,12 +128,12 @@ LzssDecodeVram: inc m0 lsr sta.l REG_VMADDL - sep #$20 + sep #$20 lda [tcc__r0], y ; copy one byte iny bcs + ; write A to vram (carry = H/L) - sta.l REG_VMDATAL + sta.l REG_VMDATAL bra ++ ; +: sta.l REG_VMDATAH ; @@ -151,7 +153,7 @@ LzssDecodeVram: phy ; preserve y sep #$20 ; y = target - disp - 1 - pha ; + pha ; and #$0F ; xba ; rep #$20 ; @@ -220,7 +222,7 @@ LzssDecodeVram: pha plb lda.l REG_VMDATAHREAD - stx REG_VMADDL + stx REG_VMADDL plb bcc @copy_writeL diff --git a/pvsneslib/source/maps.asm b/pvsneslib/source/maps.asm index 3dd782ee..642a50e3 100644 --- a/pvsneslib/source/maps.asm +++ b/pvsneslib/source/maps.asm @@ -46,6 +46,7 @@ topleft DSW MAP_MAXMTILES*4 ; a metatile is 4 8x8 pixels max .ENDST +.BASE $00 .RAMSECTION ".reg_maps" BANK 0 SLOT 1 dispxofs_L1 DW ; X scroll offset of layer 1 @@ -57,7 +58,7 @@ bgrightvramlocr_L1 DW ; VRAM word address to store the hor .ENDS -.RAMSECTION ".reg_maps7e" BANK $7E +.RAMSECTION ".reg_maps7e" BANK $7E SLOT RAMSLOT_0 metatiles INSTANCEOF metatiles_t ; entry for each metatile definition @@ -102,6 +103,7 @@ maptmpvalue DW ; TO store a temporary value (used f .ends +.BASE BASE_0 .SECTION ".maps0_text" SUPERFREE .accu 16 @@ -229,7 +231,7 @@ mapLoad: sta.l $2183 lda #$0 ; 230805 to point to correct bank for x dma value - pha + pha plb lda 16,s ; get metatiles definition bank address (11+1+2+2) @@ -255,7 +257,7 @@ mapLoad: lda #$7e ; safely use of 7E as it is explicit declared sta.l $2183 ; bank address of destination - lda 20,s + lda 20,s sta.l $4304 ; bank address of source (15+1+2+2) ldx #$8000 ; type of DMA @@ -1004,7 +1006,7 @@ mapGetMetaTilesProp: rep #$20 lda 0,x plb - and #$03FF ; to have only tile number (no flipx/y) + and #$03FF ; to have only tile number (no flipx/y) plx asl a ; property is a 16bit arrays diff --git a/pvsneslib/source/objects.asm b/pvsneslib/source/objects.asm index 6d43966b..bbeabdbe 100644 --- a/pvsneslib/source/objects.asm +++ b/pvsneslib/source/objects.asm @@ -48,12 +48,12 @@ .DEFINE T_SPIKE $0004 .DEFINE T_PLATE $0008 .DEFINE T_SLOPES $0020 -; $0020 Type Slope 1x1 Up â—¢ (action will be climb on it) for tile -; $0021 Type Slope 1x1 Down â—£ (action will be descend on it) for tile +; $0020 Type Slope 1x1 Up â—¢ (action will be climb on it) for tile +; $0021 Type Slope 1x1 Down â—£ (action will be descend on it) for tile ; $0022 Type Slope 2x1 lower half Up â—¢ (action will be climb on it) for tile ; $0023 Type Slope 2x1 lower half Down â—£ (action will be descend on it) for tile ; $0024 Type Slope 2x1 upper half Up â—¢ (action will be climb on it) for tile -; $0025 Type Slope 2x1 upper half Down â—£ (action will be descend on it) for tile +; $0025 Type Slope 2x1 upper half Down â—£ (action will be descend on it) for tile .DEFINE ACT_CLIMB $2000 .DEFINE ACT_DIE $4000 @@ -112,7 +112,8 @@ objnotused DSB 7 ; OB_SIZE-56-1 for future use .ENDST -.RAMSECTION ".reg_objects7e" bank $7E +.BASE $00 +.RAMSECTION ".reg_objects7e" BANK $7E SLOT RAMSLOT_0 objbuffers INSTANCEOF t_objs OB_MAX ; object struct in memory objactives DSW OB_TYPE_MAX ; active object list @@ -148,6 +149,7 @@ objtmp4 DW .ENDS +.BASE BASE_0 .SECTION ".objects0_text" superfree .accu 16 diff --git a/pvsneslib/source/pads.asm b/pvsneslib/source/pads.asm index a395c7a7..282d369f 100644 --- a/pvsneslib/source/pads.asm +++ b/pvsneslib/source/pads.asm @@ -1,7 +1,7 @@ ;--------------------------------------------------------------------------------- ; ; Copyright (C) 2013-2020 -; Alekmaul +; Alekmaul ; ; This software is provided 'as-is', without any express or implied ; warranty. In no event will the authors be held liable for any @@ -31,6 +31,7 @@ .equ REG_JOY1L $4218 .equ REG_JOY2L $421A +.BASE $00 .RAMSECTION ".reg_pads" BANK 0 SLOT 1 pad_keys dsb 10 ; 5 pads , 16 bits reg @@ -90,6 +91,7 @@ scope_sinceshot dsb 2 .ENDS +.BASE BASE_0 .SECTION ".pads0_text" SUPERFREE ;--------------------------------------------------------------------------------- @@ -98,18 +100,18 @@ scanPads: php phb phy - + sep #$20 ; change bank address to 0 lda.b #$0 pha plb - + rep #$20 ; copy joy states #1&2 ldy pad_keys sty pad_keysold ldy pad_keys+2 sty pad_keysold+2 - + -: lda REG_HVBJOY ; wait until joypads are ready lsr bcs - @@ -147,23 +149,23 @@ padsClear: php phb phx - + sep #$20 ; change bank address to 0 lda.b #$0 pha plb - + rep #$20 lda 8,s ; get value pha plx - + sep #$20 lda #$0 sta pad_keys,x sta pad_keysold,x sta pad_keysrepeat,x - + plx plb plp @@ -179,25 +181,25 @@ padsDown: php phb phx - + sep #$20 ; change bank address to 0 lda.b #$0 pha plb - + rep #$20 lda 8,s ; get value pha plx - + lda pad_keysold,x eor #$FFFF sta.w tcc__r0 - + lda pad_keys,x and.w tcc__r0 sta.w tcc__r0 - + plx plb plp @@ -214,26 +216,26 @@ padsUp: php phb phx - + sep #$20 ; change bank address to 0 lda.b #$0 pha plb - + rep #$20 lda 8,s ; get value pha plx - + lda pad_keys,x eor #$FFFF sta.w tcc__r0 - + lda pad_keys,x eor.w pad_keysold,x and.w tcc__r0 sta.w tcc__r0 - + plx plb plp @@ -251,7 +253,7 @@ detectMPlay5: php phb phx - + sep #$20 ; change bank address to 0 lda.b #$0 pha @@ -259,7 +261,7 @@ detectMPlay5: stz snes_mplay5 ; currently no Multiplay5 connected stz mp5read - + ldx #$8 lda.b #1 sta.w REG_JOYA ; strobe on @@ -272,16 +274,16 @@ checkmplay5ston: dex beq + asl - sta mp5read + sta mp5read bra checkmplay5ston -+ sta mp5read - ++ sta mp5read + stz.w REG_JOYA ; strobe off lda mp5read ; if not $FF, no mp5 connected cmp #$FF bne nomplay5 - + ldx #$8 stz mp5read @@ -293,9 +295,9 @@ checkmplay5stoff: dex beq + asl - sta mp5read + sta mp5read bra checkmplay5stoff -+ sta mp5read ++ sta mp5read lda mp5read ; if $FF, no mp5 connected cmp #$FF @@ -325,7 +327,7 @@ scanMPlay5: lda.b #$0 pha plb - + rep #$20 ; copy joy states #1->5 ldy pad_keys sty pad_keysold @@ -337,28 +339,28 @@ scanMPlay5: sty pad_keysold+6 ldy pad_keys+8 sty pad_keysold+8 - + -: lda REG_HVBJOY ; wait until joypads are ready lsr bcs - - sep #$20 + sep #$20 lda.b #$80 ; enable iobit to read data sta.w REG_WRIO - + lda.b #$1 sta.w REG_JOYA ; do stobe on/off - stz.w REG_JOYA + stz.w REG_JOYA rep #$20 ldy #16 getpad1data: ; get all 16 bits pad1 data serialy lda.w REG_JOYA lsr a ; put bit0 into carry - rol.w pad_keys ; pad 1 data + rol.w pad_keys ; pad 1 data dey - bne getpad1data - + bne getpad1data + ldy #16 getpad23data: ; get all 16 bits pad2&3 data serialy lda.w REG_JOYB @@ -367,11 +369,11 @@ getpad23data: ; get all 16 bits pad2&3 data serialy lsr a ; put bit1 into carry rol.w pad_keys+4 ; pad 3 data dey - bne getpad23data - + bne getpad23data + sep #$20 stz.w REG_WRIO ; to allow read for other pads - + rep #$20 ldy #16 getpad45data: ; get all 16 bits pad2&3 data serialy @@ -381,30 +383,30 @@ getpad45data: ; get all 16 bits pad2&3 data serialy lsr a ; put bit1 into carry rol.w pad_keys+8 ; pad 5 data dey - bne getpad45data + bne getpad45data lda pad_keys eor pad_keysold ; compute 'down' state from bits that and pad_keys ; have changed from 0 to 1 sta pad_keysrepeat ; lda pad_keys+2 - eor pad_keysold+2 - and pad_keys+2 + eor pad_keysold+2 + and pad_keys+2 sta pad_keysrepeat+2 lda pad_keys+4 eor pad_keysold+4 - and pad_keys+4 + and pad_keys+4 sta pad_keysrepeat+4 lda pad_keys+6 eor pad_keysold+6 - and pad_keys+6 + and pad_keys+6 sta pad_keysrepeat+6 lda pad_keys+8 eor pad_keysold+8 - and pad_keys+8 + and pad_keys+8 sta pad_keysrepeat+8 - sep #$20 + sep #$20 lda.b #$80 ; enable iobit for next frame sta.w REG_WRIO @@ -412,7 +414,7 @@ getpad45data: ; get all 16 bits pad2&3 data serialy plb plp rtl - + .ENDS .SECTION ".padsscop_text" SUPERFREE @@ -420,27 +422,27 @@ getpad45data: ; get all 16 bits pad2&3 data serialy ;--------------------------------------------------------------------------------- ; Nintendo SHVC Scope BIOS version 1.00 ; Quickly disassembled and commented by Revenant on 31 Jan 2013 -; +; ; This assembly uses xkas v14 syntax. It probably also assembles with bass, if there's ; any such thing as good fortune in the universe. ; ; How to use the SHVC Super Scope BIOS: ; (all variables are two bytes) -; +; ; 1: Set "HoldDelay" and "RepDelay" for the button hold delay and repeat rate -; +; ; 2: "jsr GetScope" or "jsl GetScopeLong" once per frame -; +; ; 3: Read one of the following to get the scope input bits (see definitions below): ; - ScopeDown (for any flags that are currently true) ; - ScopeNow (for any flags that have become true this frame) ; - ScopeHeld (for any flags that have been true for a certain length of time) ; - ScopeLast (for any flags that were true on the previous frame) -; +; ; 3a: If the bits read from ScopeNow indicate a valid shot, or if the Cursor button ; is being pressed, then read "ShotH"/"ShotV" to adjust for aim, or read ; "ShotHRaw"/"ShotVRaw" for "pure" coordinates -; +; ; 3c: at some point, set "CenterH"/"CenterV" equal to "ShotHRaw"/"ShotVRaw" ; so that the aim-adjusted coordinates are "correct" ;--------------------------------------------------------------------------------- @@ -470,7 +472,7 @@ GetScope: sta scope_shoth+1 sta scope_shothraw+1 - lda REG_OPVCT ; Get the vertical scanline location (bits 0-7) + lda REG_OPVCT ; Get the vertical scanline location (bits 0-7) sta scope_shotv sta scope_shotvraw @@ -478,7 +480,7 @@ GetScope: and.b #$01 sta scope_shotv+1 sta scope_shotvraw+1 - + rep #$20 lda scope_centerh ; Factor in the horizontal offset factor clc diff --git a/pvsneslib/source/scores.asm b/pvsneslib/source/scores.asm index 411a7be1..94344e9c 100644 --- a/pvsneslib/source/scores.asm +++ b/pvsneslib/source/scores.asm @@ -22,10 +22,12 @@ ; ;--------------------------------------------------------------------------------- +.BASE $00 .RAMSECTION ".reg_scores" BANK 0 SLOT 1 scorestring DSB 10 ; for score to string conversion .ENDS +.BASE BASE_0 .SECTION ".scores0_text" SUPERFREE ;--------------------------------------------------------------------------------- @@ -39,17 +41,17 @@ scoreClear: lda 10,s ; bank address of score pha plb - + rep #$20 ; word address of score lda 8,s tax - + lda #$0000.w ; clear score sta 0,x sta 2,x - + plx - + plb plp rtl @@ -65,7 +67,7 @@ scoreAdd: phb phx - + sep #$20 lda 10,s ; bank address of score pha @@ -74,7 +76,7 @@ scoreAdd: rep #$20 ; word address of score lda 8,s tax - + lda 12,s ; get value and add it clc adc.w 0,x @@ -82,16 +84,16 @@ scoreAdd: sec sbc #$2710 ; is it greater than 10000 ? bcc _scoAdd1 ; not a overflow, exit - + sta 0,x ; store again when 10000 is subtract lda 2,x ; add one to high number inc a sta 2,x - + _scoAdd1: - + plx - + plb plp rtl @@ -121,7 +123,7 @@ scoreCpy: pha lda 2,x pha - + sep #$20 lda 18,s ; bank address of dest score (14+4 because of pha) pha @@ -137,7 +139,7 @@ scoreCpy: sta 0,x plx - + plb plp rtl @@ -153,7 +155,7 @@ scoreCmp: phx phy - + sep #$20 lda 12,s ; bank address of source score pha @@ -162,12 +164,12 @@ scoreCmp: rep #$20 ; word address of source score lda 10,s tax - + lda 0,x ; push score on stack pha lda 2,x pha - + sep #$20 lda 20,s ; bank address of dest score pha @@ -176,7 +178,7 @@ scoreCmp: rep #$20 ; word address of dest score lda 18,s tay - + pla sec sbc 2,y ; is high equals ? @@ -191,7 +193,7 @@ _scocmpequ: beq _scocmpequ1 ; ok, return 0 bcs _scocmphig1 ; nope, it is lower bra _scocmplow1 - + _scocmpequ1: sep #$20 lda.b #0 @@ -205,7 +207,7 @@ _scocmphig1: lda.b #-1 sta.b tcc__r0 bra _scocmpbye - + _scocmplow: pla _scocmplow1: @@ -216,7 +218,7 @@ _scocmplow1: _scocmpbye: ply plx - + plb plp rtl diff --git a/pvsneslib/source/snesmodwla.asm b/pvsneslib/source/snesmodwla.asm index 3c04a24f..fdba90cb 100644 --- a/pvsneslib/source/snesmodwla.asm +++ b/pvsneslib/source/snesmodwla.asm @@ -29,9 +29,16 @@ ;---------------------------------------------------------------------- .define SB_SAMPCOUNT 8000h -.define SB_MODCOUNT 8002h -.define SB_MODTABLE 8004h -.define SB_SRCTABLE 8184h +.define SB_MODCOUNT 8002h +.define SB_MODTABLE 8004h +.define SB_SRCTABLE 8184h + +.ifdef HIROM +.redefine SB_SAMPCOUNT 0000h +.redefine SB_MODCOUNT 0002h +.redefine SB_MODTABLE 0004h +.redefine SB_SRCTABLE 0184h +.endif .equ REG_APUIO0 2140h ; Sound Register 1B/RW .equ REG_APUIO1 2141h ; Sound Register 1B/RW @@ -68,6 +75,7 @@ ;zeropage ;====================================================================== +.BASE $00 .RAMSECTION ".fp" BANK 0 SLOT 1 spc_ptr: DS 3 spc_v: DS 1 @@ -107,6 +115,7 @@ digi_copyrate: DS 1 .DEFINE SPC_BOOT 0400h ; spc entry/load address +.BASE BASE_0 .SECTION ".soundmod" SUPERFREE ;====================================================================== @@ -121,15 +130,15 @@ digi_copyrate: DS 1 ;* ;* disable time consuing interrupts during this function ;********************************************************************** -spcBoot: +spcBoot: ;---------------------------------------------------------------------- - php ; alek - sei - phb ; alek + php ; alek + sei + phb ; alek sep #$20 - lda #$0 - sta REG_NMI_TIMEN ; alek - pha + lda #$0 + sta REG_NMI_TIMEN ; alek + pha plb ; change bank address to 0 -: ldx REG_APUIO0 ; wait for 'ready signal from SPC @@ -186,13 +195,13 @@ sb_start: -: cmp REG_APUIO0 ; final sync bne - ;-------------------------------------- stz REG_APUIO0 - + stz spc_v ; reset V stz spc_q ; reset Q stz spc_fwrite ; reset command fifo stz spc_fread ; stz spc_sfx_next ; - + stz spc_pr+0 stz spc_pr+1 stz spc_pr+2 @@ -236,7 +245,7 @@ sb_start: inx cpx #$ff bne - - + ;---------------------------------------------------------------------- ; driver installation successful ;---------------------------------------------------------------------- @@ -251,7 +260,7 @@ sb_start: ; ;********************************************************************** spcSetBank: - php + php phb sep #$20 lda #$0 @@ -269,7 +278,6 @@ spcSetBank: .macro incptr iny iny - .ifndef HIROM bmi + ;_catch_overflow inc spc_ptr+2 @@ -292,9 +300,9 @@ spcSetBank: ;********************************************************************** spcLoad: ;---------------------------------------------------------------------- - php + php phb - + sep #$20 lda #$0 pha @@ -308,7 +316,7 @@ spcLoad: phx ; flush fifo! jsr xspcFlush ; plx ; - + phx ldy #SB_MODTABLE sty spc2 @@ -317,14 +325,14 @@ spcLoad: lda [spc_ptr], y ; X = MODULE SIZE ;lda spc_ptr, y ; X = MODULE SIZE tax - + incptr - + lda [spc_ptr], y ; read SOURCE LIST SIZE ;lda spc_ptr, y ; read SOURCE LIST SIZE - + incptr - + sty spc1 ; pointer += listsize*2 asl ; adc spc1 ; @@ -336,7 +344,7 @@ spcLoad: .endif inc spc_ptr+2 ; +: tay ; - + sep #20h ; lda spc_v ; wait for spc pha ; @@ -352,43 +360,43 @@ spcLoad: -: cmp REG_APUIO1 ; wait for spc bne - ;------------------------------ jsr do_transfer - + ;------------------------------------------------------ ; transfer sources ;------------------------------------------------------ - + plx ldy #SB_MODTABLE sty spc2 jsr get_address incptr - + rep #20h ; x = number of sources lda [spc_ptr], y ; tax ; - + incptr - + transfer_sources: - + lda [spc_ptr], y ; read source index sta spc1 ; - + incptr - + phy ; push memory pointer sep #20h ; and counter lda spc_ptr+2 ; pha ; phx ; - + jsr transfer_source - + plx ; pull memory pointer pla ; and counter sta spc_ptr+2 ; ply ; - + dex bne transfer_sources @no_more_sources: @@ -402,22 +410,22 @@ transfer_sources: bne - ;----------------- sta spc_pr+1 stz spc_sfx_next ; reset sfx counter - + plb plp rtl - + ;-------------------------------------------------------------- ; spc1 = source index ;-------------------------------------------------------------- transfer_source: ;-------------------------------------------------------------- - + ldx spc1 ldy #SB_SRCTABLE sty spc2 jsr get_address - + lda #01h ; port0=01h sta REG_APUIO0 ; rep #20h ; x = length (bytes->words) @@ -430,9 +438,9 @@ transfer_source: sta REG_APUIO2 incptr sep #20h - + lda spc_v ; send message - eor #80h ; + eor #80h ; ora #01h ; sta spc_v ; sta REG_APUIO1 ;----------------------- @@ -441,7 +449,7 @@ transfer_source: cpx #0 beq end_transfer ; if datalen != 0 bra do_transfer ; transfer source data - + ;-------------------------------------------------------------- ; spc_ptr+y: source address ; x = length of transfer (WORDS) @@ -464,7 +472,7 @@ do_transfer: lda spc_v ; dex ; bne transfer_again ; - + incptr end_transfer: @@ -493,13 +501,13 @@ get_address: adc spc1 ; adc spc2 ; sta spc_ptr ; - + lda [spc_ptr] ; read address pha ; sep #20h ; ldy #2 ; lda [spc_ptr],y ; read bank# - + clc ; spc_ptr = long address to module adc spc_bank ; sta spc_ptr+2 ; @@ -507,7 +515,7 @@ get_address: stz spc_ptr stz spc_ptr+1 rts ; - + ;********************************************************************** ;* x = id ;* @@ -526,7 +534,7 @@ spcLoadEffect: lda 6,s ; id tax sep #$20 - + ldy #SB_SRCTABLE ; get address of source sty spc2 ; jsr get_address ;-------------------------------------- @@ -558,15 +566,15 @@ spcLoadEffect: plb plp rtl - + ;********************************************************************** ; a = id ; spc1 = params ;********************************************************************** QueueMessage: - sei ; disable IRQ in case user + sei ; disable IRQ in case user ; has spcProcess in irq handler - + sep #10h ; queue data in fifo ldx spc_fwrite ; sta spc_fifo, x ; @@ -609,7 +617,7 @@ spcFlush1: plb plp rtl - + ;---------------------------------------------------------------------- xspcFlush: ;---------------------------------------------------------------------- @@ -619,7 +627,7 @@ xspcFlush: jsr xspcProcessMessages ; bra xspcFlush ; @exit: rts ; - + xspcProcessMessages: sep #$10 ; 8-bit index during this function @@ -683,7 +691,7 @@ spcProcess: lda #$0 pha plb ; change bank address to 0 - + lda digi_active beq spcProcessMessages jsr spcProcessStream @@ -742,7 +750,7 @@ spcProcessMessages: plb ; alek plp ; alek rtl ; alek - + ;********************************************************************** ; x = starting position ;********************************************************************** @@ -756,12 +764,12 @@ spcPlay: plb ; change bank address to 0 lda 6,s ; module_id - -; txa ; queue message: + +; txa ; queue message: sta spc1+1 ; id -- xx lda #CMD_PLAY ; jmp QueueMessage ; - + spcStop: php ; alek phb ; alek @@ -769,7 +777,7 @@ spcStop: lda #$0 pha plb ; change bank address to 0 - + lda #CMD_STOP jmp QueueMessage @@ -796,14 +804,14 @@ spcTest: ;# ;********************************************************************** spcReadStatus: ldx #5 ; read PORT2 with stability checks - lda REG_APUIO2 ; + lda REG_APUIO2 ; @loop: ; cmp REG_APUIO2 ; bne spcReadStatus ; dex ; bne @loop ; rts ; - + ;********************************************************************** ; read position register ;********************************************************************** @@ -844,7 +852,7 @@ spcSetModuleVolume: plb ; change bank address to 0 lda 6,s ; volume - + ; txa ;queue: sta spc1+1 ; id -- vv lda #CMD_MVOL ; @@ -869,7 +877,7 @@ spcFadeModuleVolume: lda 8,s ; target volume tax sep #$20 - + txa ;queue: sta spc1+1 ; id xx yy tya ; @@ -928,17 +936,17 @@ spcSetSoundTable: lda #$0 pha plb ; change bank address to 0 - + rep #$20 lda 6,s ; src (lower 16 bits) sta SoundTable sep #$20 lda 8,s ; src bank sta SoundTable+2 - + ;sty SoundTable ;sta SoundTable+2 - + plb plp;alek rtl ; return @@ -993,7 +1001,7 @@ spcPlaySound: lda #$0 pha plb ; change bank address to 0 - + lda 6,s ; index of sound xba @@ -1002,7 +1010,7 @@ spcPlaySound: ldx #-1 ldy #-1 jmp spcPlaySoundEx - + ;====================================================================== spcPlaySoundV: ;====================================================================== @@ -1013,19 +1021,19 @@ spcPlaySoundV: lda #$0 pha plb ; change bank address to 0 - + rep #$20 lda 6,s ; volume of sound tay sep #$20 lda 8,s ; index of sound - + xba lda #128 xba ldx #-1 jmp spcPlaySoundEx - + ;---------------------------------------------------------------------- ; a = index ; b = pitch @@ -1039,9 +1047,9 @@ spcPlaySoundEx: phx ; ;---------------------------------------------------------------------------- rep #30h ; um - pha ; + pha ; ;---------------------------------------------------------------------------- - and #0FFh ; y = sound table index + and #0FFh ; y = sound table index asl ; asl ; asl ; @@ -1053,7 +1061,7 @@ spcPlaySoundEx: sep #20h ; ;---------------------------------------------------------------------------- cmp #0 ; if a < 0 then use default - bmi @use_default_pitch ; otherwise use direct + bmi @use_default_pitch ; otherwise use direct sta digi_pitch ; bra @direct_pitch ; @use_default_pitch: ; @@ -1085,7 +1093,7 @@ spcPlaySoundEx: ;---------------------------------------------------------------------------- asl ; vp = (vol << 4) | pan asl ; - asl ; + asl ; asl ; ora spc1 ; sta digi_vp ; @@ -1111,13 +1119,13 @@ spcPlaySoundEx: ;---------------------------------------------------------------------------- lda #1 ; set flags sta digi_init ; - sta digi_active ; + sta digi_active ; ;---------------------------------------------------------------------------- ; rts plb plp ; alek rtl; alek - + ;============================================================================ spcProcessStream: ;============================================================================ @@ -1155,7 +1163,7 @@ spcProcessStream: lda digi_copyrate ; get copy rate @newnote: rep #20h ; saturate against remaining length - and #0FFh ; + and #0FFh ; cmp digi_remain ; bcc @nsatcopy ; lda digi_remain ; @@ -1187,7 +1195,7 @@ spcProcessStream: @next_block: - + lda [digi_src2], y sta spc2 rep #20h ; read 2 bytes @@ -1208,7 +1216,7 @@ spcProcessStream: ;----------------------------------------------------------------------- -: cpx REG_APUIO0 ; wait for spc bne - ; -;-----------------------------------------------------------------------: +;-----------------------------------------------------------------------: lda spc_pr+0 ; restore port data sta REG_APUIO0 ; lda spc_pr+1 ; @@ -1229,7 +1237,7 @@ spcProcessStream: sep #20h ; ;----------------------------------------------------------------------- rts - + digi_rates: .db 0, 3, 5, 7, 9, 11, 13 diff --git a/pvsneslib/source/sounds.asm b/pvsneslib/source/sounds.asm index 0e0136d0..f37ed710 100644 --- a/pvsneslib/source/sounds.asm +++ b/pvsneslib/source/sounds.asm @@ -22,13 +22,14 @@ ; ;--------------------------------------------------------------------------------- +.BASE $00 .RAMSECTION ".reg_sounds" BANK 0 SLOT 1 snds_val1 DSB 2 ; save value #1 .ENDS - +.BASE BASE_0 .SECTION ".sound0_text" SUPERFREE .accu 16 diff --git a/pvsneslib/source/sprites.asm b/pvsneslib/source/sprites.asm index 2561c6d4..fe02a4d4 100644 --- a/pvsneslib/source/sprites.asm +++ b/pvsneslib/source/sprites.asm @@ -46,7 +46,8 @@ oamgraphics DSB 4 ; 8..11 pointer to graphic file dummy DSB 4 ; 12..15 to by align to 16 bytes .ENDST -.RAMSECTION ".reg_oams7e" BANK $7E +.BASE $00 +.RAMSECTION ".reg_oams7e" BANK $7E SLOT RAMSLOT_0 sprit_val0 DB ; save value #0 sprit_val1 DB ; save value #1 @@ -79,6 +80,7 @@ sprbyte4 DB .ENDS +.BASE BASE_0 .SECTION ".sprites0_text" SUPERFREE .ACCU 16 @@ -855,7 +857,7 @@ lkup8oamS: ; lookup table for 8x8 sprites in VRAM (128 sprites max $0000->$1000 .word $0A00,$0A20,$0A40,$0A60,$0A80,$0AA0,$0AC0,$0AE0,$0B00,$0B20,$0B40,$0B60,$0B80,$0BA0,$0BC0,$0BE0 .word $0C00,$0C20,$0C40,$0C60,$0C80,$0CA0,$0CC0,$0CE0,$0D00,$0D20,$0D40,$0D60,$0D80,$0DA0,$0DC0,$0DE0 .word $0E00,$0E20,$0E40,$0E60,$0E80,$0EA0,$0EC0,$0EE0,$0F00,$0F20,$0F40,$0F60,$0F80,$0FA0,$0FC0,$0FE0 -lkup8idT: ; lookup table for 8x8 sprites ID identification +lkup8idT: ; lookup table for 8x8 sprites ID identification .word $0100,$0101,$0102,$0103,$0104,$0105,$0106,$0107,$0108,$0109,$010A,$010B,$010C,$010D,$010E,$010F .word $0110,$0111,$0112,$0113,$0114,$0115,$0116,$0117,$0118,$0119,$011A,$011B,$011C,$011D,$011E,$011F .word $0120,$0121,$0122,$0123,$0124,$0125,$0126,$0127,$0128,$0129,$012A,$012B,$012C,$012D,$012E,$012F @@ -1308,7 +1310,7 @@ oamDynamic32Draw: lda 10,s ; get id asl a ; to be on correct index (16 bytes per oam) asl a - asl a + asl a asl a tay @@ -1429,7 +1431,7 @@ _o32DRep3: clc adc #$0004 sta.w oamnumberperframe - + ply plx @@ -1458,7 +1460,7 @@ oamDynamic16Draw: lda 10,s ; get id asl a ; to be on correct index (16 bytes per oam) asl a - asl a + asl a asl a tay @@ -1606,7 +1608,7 @@ _o16DRep2p: clc adc #$0004 sta.w oamnumberperframe - + ply plx @@ -1635,7 +1637,7 @@ oamDynamic8Draw: lda 10,s ; get id asl a ; to be on correct index (16 bytes per oam) asl a - asl a + asl a asl a tay @@ -1912,9 +1914,9 @@ oamSort: ; lda #TABLE_SIZE - 1 ; Initialize high index ; ldx #0 ; Initialize low index - + jsr quicksort ; Perform quicksort - + plx plb plp @@ -1925,67 +1927,67 @@ quicksort: ; Quicksort algorithm pha ; Save registers phx phy - + ; LDX #VAR1_OFFSET ; Sort based on VAR1 - + ; LDA HIGH_IDX ; Load high index ; STA TEMP_HIGH - + ; LDA LOW_IDX ; Load low index ; STA TEMP_LOW - + ; LDX TEMP_LOW ; Load low index into X - + ; INX ; Increment low index - + ; LDA TABLE, X ; Load pivot element ; STA PIVOT - + ; Partition the table partition_loop: ; LDA TABLE, X ; Load element for comparison ; CMP PIVOT - + BCC increment_low_index ; If element is less than pivot, increment low index - + ; Swap elements ; LDA TABLE, X ; STA TABLE, TEMP_HIGH ; LDA TABLE, TEMP_LOW ; STA TABLE, X - + ; DEC TEMP_HIGH ; Decrement high index ; DEY ; Decrement Y - + ; Check if all elements have been partitioned ; CPX TEMP_HIGH ; BCC partition_loop ; If not, continue partitioning - + ; Swap pivot element into its correct position ; LDA TABLE, X ; STA TABLE, TEMP_HIGH ; LDA PIVOT ; STA TABLE, X - + ; Recursive calls to quicksort ; LDA TEMP_LOW ; CMP LOW_IDX ; BCC skip_left ; STA HIGH_IDX ; JSR quicksort - + skip_left: ; LDA TEMP_HIGH ; CMP HIGH_IDX ; BCC skip_right ; STA LOW_IDX ; JSR quicksort - + skip_right: ply plx pla - + rts ; Return from subroutine diff --git a/pvsneslib/source/videos.asm b/pvsneslib/source/videos.asm index ddf0b2d6..6dea0305 100644 --- a/pvsneslib/source/videos.asm +++ b/pvsneslib/source/videos.asm @@ -70,8 +70,8 @@ .EQU MOSAIC_IN 2 .EQU MOSAIC_OUT 1 - -.RAMSECTION ".reg_video7e" BANK $7E +.BASE $00 +.RAMSECTION ".reg_video7e" BANK $7E SLOT RAMSLOT_0 videoMode DSB 1 videoModeSub DSB 1 @@ -81,7 +81,7 @@ iloc DSB 1 .ENDS -.RAMSECTION ".reg_video7e_matrix" BANK $7E +.RAMSECTION ".reg_video7e_matrix" BANK $7E SLOT RAMSLOT_0 m7ma DSB 2 m7mb DSB 2 @@ -101,6 +101,7 @@ m7_md DSB (225-64)*3 ; 483 bytes .ENDS +.BASE BASE_0 .SECTION ".videos0_text" SUPERFREE .ACCU 16 @@ -124,7 +125,7 @@ setFadeEffect: ldx.b #$0 -: - wai + wai txa sta.l REG_INIDISP inx @@ -141,7 +142,7 @@ setFadeEffect: _fadeouteffect: ldx.b #$F -: - wai + wai txa sta.l REG_INIDISP dex @@ -1003,7 +1004,7 @@ setMode7Scale: rep #$20 ; get xscale lda 5,s - + sep #$20 ; REG_M7A = xscale; sta.l REG_M7A rep #$20 @@ -1013,7 +1014,7 @@ setMode7Scale: rep #$20 ; get yscale lda 7,s - + sep #$20 ; REG_M7D = yscale; sta.l REG_M7D rep #$20 diff --git a/snes-examples/memory_mapping/Makefile b/snes-examples/memory_mapping/Makefile new file mode 100644 index 00000000..9a2d85aa --- /dev/null +++ b/snes-examples/memory_mapping/Makefile @@ -0,0 +1,27 @@ +ifeq ($(strip $(PVSNESLIB_HOME)),) +$(error "Please create an environment variable PVSNESLIB_HOME by following this guide: https://github.com/alekmaul/pvsneslib/wiki/Installation") +endif + +# Tell the compiler to export a Mode 21 (HiROM) memory mapped ROM +HIROM=1 +# Tell the compiler to use FastROM. $4206 reg is enabled on start and reset, nmi and vectors jumps with an ofset of $80 banks +FASTROM=1 + +include ${PVSNESLIB_HOME}/devkitsnes/snes_rules + +.PHONY: bitmaps all + +#--------------------------------------------------------------------------------- +# ROMNAME is used in snes_rules file +export ROMNAME := memory_mapping + +all: bitmaps $(ROMNAME).sfc + +clean: cleanBuildRes cleanRom cleanGfx + +#--------------------------------------------------------------------------------- +pvsneslibfont.pic: pvsneslibfont.png + @echo convert font with no tile reduction ... $(notdir $@) + $(GFXCONV) -s 8 -o 16 -u 16 -p -e 0 -i $< + +bitmaps : pvsneslibfont.pic diff --git a/snes-examples/memory_mapping/data.asm b/snes-examples/memory_mapping/data.asm new file mode 100644 index 00000000..c008225b --- /dev/null +++ b/snes-examples/memory_mapping/data.asm @@ -0,0 +1,11 @@ +.include "hdr.asm" + +.section ".rodata1" superfree + +tilfont: +.incbin "pvsneslibfont.pic" + +palfont: +.incbin "pvsneslibfont.pal" + +.ends diff --git a/snes-examples/memory_mapping/hdr.asm b/snes-examples/memory_mapping/hdr.asm new file mode 100644 index 00000000..26975f79 --- /dev/null +++ b/snes-examples/memory_mapping/hdr.asm @@ -0,0 +1,87 @@ +.define HIROM 1 +.define FASTROM 1 + +.ifndef HIROM ;==LoRom== + +.MEMORYMAP ; Begin describing the system architecture. + SLOTSIZE $8000 ; The slot is $8000 bytes in size. More details on slots later. + DEFAULTSLOT 0 ; There's only 1 slot in SNES, there are more in other consoles. + SLOT 0 $8000 ; Defines Slot 0's starting address. + SLOT 1 $0 $2000 + SLOT 2 $2000 $E000 + SLOT 3 $0 $10000 +.ENDME ; End MemoryMap definition + +.ROMBANKSIZE $8000 ; Every ROM bank is 32 KBytes in size + +.else ;==HiRom== + +.MEMORYMAP ; Begin describing the system architecture. + SLOTSIZE $10000 ; The slot is $10000 bytes in size. More details on slots later. + DEFAULTSLOT 0 ; There's only 1 slot in SNES, there are more in other consoles. + SLOT 0 $0000 ; Defines Slot 0's starting address. + SLOT 1 $0 $2000 + SLOT 2 $2000 $E000 + SLOT 3 $0 $10000 + SLOT 4 $6000 ; Used for direct SRAM access. +.ENDME ; End MemoryMap definition + +.ROMBANKSIZE $10000 ; Every ROM bank is 32 KBytes in size + +.endif + +.ROMBANKS 8 ; 2 Mbits - Tell WLA we want to use 8 ROM Banks + +.SNESHEADER + ID "SNES" ; 1-4 letter string, just leave it as "SNES" + + NAME "LIBSNES HIROM MEM MAP" ; Program Title - can't be over 21 bytes, + ; "123456789012345678901" ; use spaces for unused bytes of the name. + +.ifdef FASTROM + FASTROM +.else + SLOWROM +.endif + +.ifdef HIROM + HIROM +.else + LOROM +.endif + + CARTRIDGETYPE $00 ; $00=ROM, $01=ROM+RAM, $02=ROM+SRAM, $03=ROM+DSP1, $04=ROM+RAM+DSP1, $05=ROM+SRAM+DSP1, $13=ROM+Super FX + ROMSIZE $08 ; $08=2 Megabits, $09=4 Megabits,$0A=8 Megabits,$0B=16 Megabits,$0C=32 Megabits + SRAMSIZE $00 ; $00=0 kilobits, $01=16 kilobits, $02=32 kilobits, $03=64 kilobits + COUNTRY $01 ; $01= U.S., $00=Japan, $02=Europe, $03=Sweden/Scandinavia, $04=Finland, $05=Denmark, $06=France, $07=Netherlands, $08=Spain, $09=Germany, $0A=Italy, $0B=China, $0C=Indonesia, $0D=Korea + LICENSEECODE $00 ; Just use $00 + VERSION $00 ; $00 = 1.00, $01 = 1.01, etc. +.ENDSNES + +.SNESNATIVEVECTOR ; Define Native Mode interrupt vector table + COP EmptyHandler + BRK EmptyHandler + ABORT EmptyHandler + NMI VBlank + IRQ EmptyHandler +.ENDNATIVEVECTOR + +.SNESEMUVECTOR ; Define Emulation Mode interrupt vector table + COP EmptyHandler + ABORT EmptyHandler + NMI EmptyHandler + RESET tcc__start ; where execution starts + IRQBRK EmptyHandler +.ENDEMUVECTOR + +.ifdef FASTROM +.ifdef HIROM +.BASE $C0 +.else +.BASE $80 +.endif +.else +.ifdef HIROM +.BASE $40 +.endif +.endif \ No newline at end of file diff --git a/snes-examples/memory_mapping/pvsneslibfont.png b/snes-examples/memory_mapping/pvsneslibfont.png new file mode 100644 index 0000000000000000000000000000000000000000..043dbf23ccfbd05ecb4cf639ace4ee8efffdcbac GIT binary patch literal 1739 zcmeAS@N?(olHy`uVBq!ia0y~yUFdh=jFX>*Uw6)z6Lml# z@eXI(MdpERDYNG( zM|+>;`7ue=IjC!G?(F@46@I_xT=ack;J$aS{+D_FKg;*befzSz$0z(N+s-j#%l(4) z#*FnhzvVN*IwF#>JxcYUaDAN_Y>+wlhF4lcl zIPz}KyC}VDp_U!@CH|HS4<^CV%a^r2v^}@tGy3)FxZuoVxw4g0Q)FZomc-wftzn`f zf60d_t^Lo$5}C~9FSi)vI`==k#P?(63U)i0M>X%vD=K%dDQ&u={Pfkae~;vfY_~+~MW7N^`-=JLo#Ov+9Z7S@t@AAYxyB)Xg^B()ZSNPR)=X9{Iu#RmyFMZonPU}@f zt>^osm(%1WZN8=(e`EH!$h2tlp9A+FtNi4AXPJBGdEa@?sV@0% zhKH8F_P;*i?q%yAb#cO)FZ{F43;*0(!R{oOUJ!fkRp0Bl!#aPB-$Xp$GvmP-?#-<4 zrP^O#YP;qBn6<3Y;kIC;(5EfFtGBvJ?TYW)ezhMC^leypkg=%g`KVV?WWHUH1Q zKD+NB|F34g|6#YLfBw7a`q#MyKguUhuW$1@S$}cww9O^=8|9xy8vfd0x!D|4DtNm3 KxvX + +extern char tilfont, palfont; + +//--------------------------------------------------------------------------------- +int main(void) +{ + // Initialize SNES + consoleInit(); + + // Initialize text console with our font + consoleSetTextVramBGAdr(0x6800); + consoleSetTextVramAdr(0x3000); + consoleSetTextOffset(0x0100); + consoleInitText(0, 16 * 2, &tilfont, &palfont); + + // Init background + bgSetGfxPtr(0, 0x2000); + bgSetMapPtr(0, 0x6800, SC_32x32); + + // Now Put in 16 color mode and disable Bgs except current + setMode(BG_MODE1, 0); + bgSetDisable(1); + bgSetDisable(2); + + // Draw a wonderful text :P + consoleDrawText(4, 13, "This is a HiROM-FastROM"); + consoleDrawText(10, 15, "mapped ROM!"); + + // Wait for nothing :P + setScreenOn(); + + while (1) + { + WaitForVBlank(); + } + return 0; +} \ No newline at end of file From 0517562873dc9ec8391826041dec563f824c3a14 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Wed, 20 Mar 2024 05:41:22 +0100 Subject: [PATCH 005/106] fix(scr): change screen coordinates tests for object --- pvsneslib/source/objects.asm | 53 +++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/pvsneslib/source/objects.asm b/pvsneslib/source/objects.asm index 6d43966b..a605e389 100644 --- a/pvsneslib/source/objects.asm +++ b/pvsneslib/source/objects.asm @@ -715,9 +715,9 @@ _oiual3: sbc.l x_pos cmp.w #OB_SCR_XRR_CHK - bcc _oiual3y ; x is lower than max + bcc _oiual3y ; x is lower than map max cmp.w #OB_SCR_XLR_CHK - bcc _oiual3y1 ; but x is lower than min + bcc _oiual3y1 ; but x is lower than map min _oiual3y: ; check now y coordinates lda objbuffers.1.ypos+1,x @@ -725,25 +725,58 @@ _oiual3y: ; check now y coordinate sbc.l y_pos cmp.w #OB_SCR_YRR_CHK - bcc _oiual32 ; y is lower than max + bcc _oiual32 ; y is lower than map max cmp.w #OB_SCR_YLR_CHK - bcs _oiual32 ; but y is greater than min + bcs _oiual32 ; but y is greater than map min _oiual3y1: +; sep #$20 ; check if it was previously on screen to refresh all the objects +; lda objbuffers.1.onscreen,x +; beq _oiual3y11 ; no ? no need to refresh +; stz objbuffers.1.onscreen,x +; lda objneedrefresh ; if we have noticed a global refresh previously, don't do it again +; bne _oiual3y11 +; lda #1 +; sta objneedrefresh +; jsr objOamRefreshAll ; do a global refresh of sprites + +_oiual3y11: + jmp _oial4 + +_oiual32: ; *** now we test if we are really on screen *** + lda objbuffers.1.xpos+1,x + sec + sbc.l x_pos + cmp.w #OB_SCR_XRI_CHK + bcc _oiual3sy ; x is lower than map max + cmp.w #OB_SCR_XLE_CHK + bcc _oiual3sy1 ; but x is lower than map min + +_oiual3sy: ; check now y coordinates + lda objbuffers.1.ypos+1,x + sec + sbc.l y_pos + + cmp.w #OB_SCR_YRI_CHK + bcc _oiuals32 ; y is lower than map max + cmp.w #OB_SCR_YLE_CHK + bcs _oiuals32 ; but y is greater than map min + +_oiual3sy1: sep #$20 ; check if it was previously on screen to refresh all the objects lda objbuffers.1.onscreen,x - beq _oiual3y11 ; no ? no need to refresh + beq _oiual3sy11 ; no ? no need to refresh stz objbuffers.1.onscreen,x lda objneedrefresh ; if we have noticed a global refresh previously, don't do it again - bne _oiual3y11 + bne _oiual3sy11 lda #1 sta objneedrefresh jsr objOamRefreshAll ; do a global refresh of sprites -_oiual3y11: - bra _oial4 +_oiual3sy11: ; here we are not on screen but we manage object update + bra _oiual321 -_oiual32: +_oiuals32: sep #$20 lda objbuffers.1.onscreen,x bne _oiual321 ; no ? we need to refresh @@ -2016,7 +2049,7 @@ _oiloend: .SECTION ".objects4_text" superfree ;--------------------------------------------------------------------------------- -; void objCollidObj(u16 objidx,u16 obj1idx) +; u16 objCollidObj(u16 objhandle1, u16 objhandle2); ; 5-6 7-8 objCollidObj: php From 28970974f16a3f52e6767a6d004b8197dd621c72 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Wed, 20 Mar 2024 05:42:01 +0100 Subject: [PATCH 006/106] chore(*): add fix function to allow sprite to be load directly from vram memory --- pvsneslib/include/snes/sprite.h | 29 ++- pvsneslib/source/sprites.asm | 334 +++++++++++++++++++++++++++++++- 2 files changed, 360 insertions(+), 3 deletions(-) diff --git a/pvsneslib/include/snes/sprite.h b/pvsneslib/include/snes/sprite.h index 69e2b9ad..c89229aa 100644 --- a/pvsneslib/include/snes/sprite.h +++ b/pvsneslib/include/snes/sprite.h @@ -2,7 +2,7 @@ sprite.h -- definitions for SNES sprites - Copyright (C) 2012-2017 + Copyright (C) 2012-2024 Alekmaul This software is provided 'as-is', without any express or implied @@ -290,16 +290,43 @@ void oamVramQueueUpdate(void); */ void oamDynamic32Draw(u16 id); +/*!\brief Add a 32x32 sprite on screen.
oambuffer[id] needs to be populate before. + !\brief Sprite is never refresh (VRAM must be update before) + !\brief oamframeid is the VRAM entry number for sprite (eg.4 for VRAM offset 0x0040, 15 for 0x0CC...) + !\brief WARNING: as the sprite engine begin at VRAM address 0x0000, you need to take care of High bit + !\brief if you want to use a VRAM entry number more than 0x00FF + \param id the oam number to be used [0 - 127] +*/ +void oamFix32Draw(u16 id); + /*!\brief Add a 16x16 sprite on screen.
oambuffer[id] needs to be populate before. \param id the oam number to be used [0 - 127] */ void oamDynamic16Draw(u16 id); +/*!\brief Add a 16x16 sprite on screen.
oambuffer[id] needs to be populate before. + !\brief Sprite is never refresh (VRAM must be update before) + !\brief oamframeid is the VRAM entry number for sprite (eg.4 for VRAM offset 0x0040, 15 for 0x0CC...) + !\brief WARNING: as the sprite engine begin at VRAM address 0x0000, you need to take care of High bit + !\brief if you want to use a VRAM entry number more than 0x00FF + \param id the oam number to be used [0 - 127] +*/ +void oamFix16Draw(u16 id); + /*!\brief Add a 8x8 sprite on screen.
oambuffer[id] needs to be populate before. \param id the oam number to be used [0 - 127] */ void oamDynamic8Draw(u16 id); +/*!\brief Add a 8x8 sprite on screen.
oambuffer[id] needs to be populate before. + !\brief Sprite is never refresh (VRAM must be update before) + !\brief oamframeid is the VRAM entry number for sprite (eg.4 for VRAM offset 0x0040, 15 for 0x0CC...) + !\brief WARNING: as the sprite engine begin at VRAM address 0x0000, you need to take care of High bit + !\brief if you want to use a VRAM entry number more than 0x00FF + \param id the oam number to be used [0 - 127] +*/ +void oamFix8Draw(u16 id); + /*!\brief Add a Meta sprite on screen (can be composed of 8x8,16x16 or 32x32 sprites).
oambuffer[id] needs to be populate before. \param id the oam number to be used [0 - 127]. \param x x coordinate of the metasprite diff --git a/pvsneslib/source/sprites.asm b/pvsneslib/source/sprites.asm index 2561c6d4..d8af9b19 100644 --- a/pvsneslib/source/sprites.asm +++ b/pvsneslib/source/sprites.asm @@ -1,6 +1,6 @@ ;--------------------------------------------------------------------------------- ; -; Copyright (C) 2012-2022 +; Copyright (C) 2012-2024 ; Alekmaul ; ; This software is provided 'as-is', without any express or implied @@ -1423,7 +1423,7 @@ _o32DRep3: sta oamMemory,y ; store new value in oam table #2 rep #$20 - inc.w oamnumberspr0 ; one more sprite 8x8 + inc.w oamnumberspr0 ; one more sprite 32x32 lda oamnumberperframe ; go to next sprite entry (x4 multiplier) clc @@ -1439,6 +1439,112 @@ _o32DRep3: .ENDS +.SECTION ".sprites8f_text" SUPERFREE + +;--------------------------------------------------------------------------------- +; void oamFix32Draw(u16 id) { +oamFix32Draw: + php + phb + phx + phy + + sep #$20 + lda #$7e + pha + plb + + rep #$20 + lda 10,s ; get id + asl a ; to be on correct index (16 bytes per oam) + asl a + asl a + asl a + + tay + ldx oamnumberperframe ; get current sprite number (x4 entry) + + phx + lda oambuffer.1.oamframeid,y ; get graphics offset of 32x32 sprites + asl a + tax + lda.l lkup32idT,x + plx + sta.w oamMemory+2,x ; store in oam memory + + lda oambuffer.1.oamx,y ; get x coordinate + xba ; save it + sep #$20 ; A 8 bits + ror a ; x msb into carry (saved if x>255) + + lda oambuffer.1.oamy,y ; get y coordinate + xba + rep #$20 ; A 8 bits + sta.w oamMemory,x ; store x & y in oam memory + + lda.w #$0200 ; put $02 into MSB + sep #$20 ; A 8 bits + lda.l oammask+2,x ; get offset in the extra OAM data + phy + tay + + bcs _o32FRep3 ; if no x<255, no need to update + + lda.l oammask+1,x + and.w oamMemory,y + sta.w oamMemory,y ; store x in oam memory + brl + + +_o32FRep3: + lda.l oammask,x + ora.w oamMemory,y + sta.w oamMemory,y ; store x in oam memory + ++: + ply + lda oambuffer.1.oamattribute,y ; get attr + sta oamMemory+3,x ; store attributes in oam memory + + rep #$20 ; oamSetEx(nb_sprites_oam, OBJ_SMALL, OBJ_SHOW); + lda oamnumberperframe ; always small for 8px sprites + lsr a + lsr a + lsr a + lsr a + clc + adc.w #512 ; id>>4 + 512 + tay ; oam pointer is now on oam table #2 + + lda oamnumberperframe ; id + lsr a + lsr a + and.w #$0003 ; id >> 2 & 3 + tax + + sep #$20 + lda oamMemory,y ; get value of oam table #2 + and.l oamSetExand,x + sta oamMemory,y ; store new value in oam table #2 + + lda.l oamSizeshift,x ; get shifted value of hide (<<1, <<3, <<5, <<7 + ora oamMemory,y + sta oamMemory,y ; store new value in oam table #2 + + rep #$20 + lda oamnumberperframe ; go to next sprite entry (x4 multiplier) + clc + adc #$0004 + sta.w oamnumberperframe + + ply + plx + + plb + plp + rtl + +.ENDS + .SECTION ".sprites9_text" SUPERFREE ;--------------------------------------------------------------------------------- @@ -1616,6 +1722,128 @@ _o16DRep2p: .ENDS +.SECTION ".sprites9f_text" SUPERFREE + +;--------------------------------------------------------------------------------- +; void oamFix16Draw(u16 id) { +oamFix16Draw: + php + phb + phx + phy + + sep #$20 + lda #$7e + pha + plb + + rep #$20 + lda 10,s ; get id + asl a ; to be on correct index (16 bytes per oam) + asl a + asl a + asl a + + tay + ldx oambuffer.1.oamframeid,y ; get current sprite number (x4 entry) + + phx + lda spr16addrgfx ; if large sprite, adjust address + cmp spr0addrgfx + beq + + lda oamnumberspr1 ; get address if not big sprite + asl a + tax + lda.l lkup16idT,x + brl _o16DRep1p + ++: lda oamnumberspr0 ; get address if big sprite + asl a + tax + lda.l lkup16idT0,x +_o16DRep1p: + plx + sta.w oamMemory+2,x ; store in oam memory + + lda oambuffer.1.oamx,y ; get x coordinate + xba ; save it + sep #$20 ; A 8 bits + ror a ; x msb into carry (saved if x>255) + + lda oambuffer.1.oamy,y ; get y coordinate + xba + rep #$20 ; A 16 bits + sta.w oamMemory,x ; store x & y in oam memory + + lda.w #$0200 ; put $02 into MSB + sep #$20 ; A 8 bits + lda.l oammask+2,x ; get offset in the extra OAM data + phy + tay + + bcs _o16DRep3 ; if no x<255, no need to update + + lda.l oammask+1,x + and.w oamMemory,y + sta.w oamMemory,y ; store x in oam memory + brl + + +_o16DRep3: + lda.l oammask,x + ora.w oamMemory,y + sta.w oamMemory,y ; store x in oam memory + ++: + ply + lda oambuffer.1.oamattribute,y ; get attr + sta oamMemory+3,x ; store attributes in oam memory + + rep #$20 ; oamSetEx(nb_sprites_oam, OBJ_SMALL, OBJ_SHOW); + lda oamnumberperframe + lsr a + lsr a + lsr a + lsr a + clc + adc.w #512 ; id>>4 + 512 + tay ; oam pointer is now on oam table #2 + + lda oamnumberperframe ; id + lsr a + lsr a + and.w #$0003 ; id >> 2 & 3 + tax + + sep #$20 + lda oamMemory,y ; get value of oam table #2 + and.l oamSetExand,x + sta oamMemory,y ; store new value in oam table #2 + + rep #$20 + lda spr16addrgfx ; if large sprite, adjust it + cmp spr1addrgfx + beq _o16DRep2p + sep #$20 + lda.l oamSizeshift,x ; get shifted value of hide (<<1, <<3, <<5, <<7 + ora oamMemory,y + sta oamMemory,y ; store new value in oam table #2 + rep #$20 + +_o16DRep2p: + lda oamnumberperframe ; go to next sprite entry (x4 multiplier) + clc + adc #$0004 + sta.w oamnumberperframe + + ply + plx + + plb + plp + rtl + +.ENDS + .SECTION ".spritesa_text" SUPERFREE ;--------------------------------------------------------------------------------- @@ -1761,6 +1989,108 @@ _o8DRep3: .ENDS +.SECTION ".spritesaf_text" SUPERFREE + +;--------------------------------------------------------------------------------- +; void oamFix8Draw(u8 id) { +oamFix8Draw: + php + phb + phx + phy + + sep #$20 + lda #$7e + pha + plb + + rep #$20 + lda 10,s ; get id + asl a ; to be on correct index (16 bytes per oam) + asl a + asl a + asl a + + tay + ldx oamnumberperframe ; get current sprite number (x4 entry) + + phx + lda oambuffer.1.oamframeid,y ; get graphics offset of 8x8 sprites + asl a + tax + lda.l lkup8idT,x + plx + sta.w oamMemory+2,x ; store in oam memory + + lda oambuffer.1.oamx,y ; get x coordinate + xba ; save it + sep #$20 ; A 8 bits + ror a ; x msb into carry (saved if x>255) + + lda oambuffer.1.oamy,y ; get y coordinate + xba + rep #$20 ; A 8 bits + sta.w oamMemory,x ; store x & y in oam memory + + lda.w #$0200 ; put $02 into MSB + sep #$20 ; A 8 bits + lda.l oammask+2,x ; get offset in the extra OAM data + phy + tay + + bcs _o8DRep3 ; if no x<255, no need to update + + lda.l oammask+1,x + and.w oamMemory,y + sta.w oamMemory,y ; store x in oam memory + brl + + +_o8DRep3: + lda.l oammask,x + ora.w oamMemory,y + sta.w oamMemory,y ; store x in oam memory + ++: + ply + lda oambuffer.1.oamattribute,y ; get attr + sta oamMemory+3,x ; store attributes in oam memory + + rep #$20 ; oamSetEx(nb_sprites_oam, OBJ_SMALL, OBJ_SHOW); + lda oamnumberperframe ; always small for 8px sprites + lsr a + lsr a + lsr a + lsr a + clc + adc.w #512 ; id>>4 + 512 + tay ; oam pointer is now on oam table #2 + + lda oamnumberperframe ; id + lsr a + lsr a + and.w #$0003 ; id >> 2 & 3 + tax + + sep #$20 + lda oamMemory,y ; get value of oam table #2 + and.l oamSetExand,x + sta oamMemory,y ; store new value in oam table #2 + + rep #$20 + lda oamnumberperframe ; go to next sprite entry (x4 multiplier) + clc + adc #$0004 + sta.w oamnumberperframe + + ply + plx + + plb + plp + rtl + +.ENDS + .SECTION ".spritesb_text" SUPERFREE ;--------------------------------------------------------------------------------- From e5d50c223740a89bee0c8c099d801ab20f225e9b Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Wed, 20 Mar 2024 05:52:25 +0100 Subject: [PATCH 007/106] chore(*): change tcc version --- compiler/tcc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/tcc b/compiler/tcc index 5ddc9b45..76749ba1 160000 --- a/compiler/tcc +++ b/compiler/tcc @@ -1 +1 @@ -Subproject commit 5ddc9b45467e7d8195a85155ba4a728ba0da6bca +Subproject commit 76749ba1969dd0417d9a4a44754a1ebbde0efba1 From 564dab081822ee348b3a3f121ac654e1ceb32dc4 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Fri, 22 Mar 2024 07:23:31 +0100 Subject: [PATCH 008/106] chore(dbg): remove dbg files when cleaning --- devkitsnes/snes_rules | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/devkitsnes/snes_rules b/devkitsnes/snes_rules index f6227f6f..c3925f59 100644 --- a/devkitsnes/snes_rules +++ b/devkitsnes/snes_rules @@ -100,6 +100,11 @@ SFILES := $(SFILES) $(wildcard $(SRC)/*.asm) SFILES := $(SFILES) $(wildcard $(SRC)/*/*.asm) SFILES := $(SFILES) $(wildcard $(SRC)/*/*/*.asm) +DBGFILES = $(wildcard *.dbg) +DBGFILES+= $(wildcard $(SRC)/*.dbg) +DBGFILES+= $(wildcard $(SRC)/*/*.dbg) +DBGFILES+= $(wildcard $(SRC)/*/*/*.dbg) + export OFILES := $(BINFILES:.bin=.obj) $(CFILES:.c=.obj) $(SFILES:.asm=.obj) # The first rule available in makefile become the default one @@ -189,7 +194,7 @@ $(SOUNDBANK).asm : $(AUDIOFILES) cleanBuildRes: cleanDebug @echo clean build resources - @rm -f $(OFILES) linkfile + @rm -f $(OFILES) $(DBGFILES) linkfile cleanRom: @echo clean rom From 772c67d4570df99900704d4317df2318109ffa0b Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Fri, 22 Mar 2024 07:24:04 +0100 Subject: [PATCH 009/106] fix(lib):: remove lib folder when cleaning --- pvsneslib/Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvsneslib/Makefile b/pvsneslib/Makefile index 8bc40686..04aba842 100644 --- a/pvsneslib/Makefile +++ b/pvsneslib/Makefile @@ -6,8 +6,8 @@ export TOPDIR := $(CURDIR) # create version number which will be used everywhere export PVSNESLIB_MAJOR := 4 -export PVSNESLIB_MINOR := 2 -export PVSNESLIB_PATCH := 1 +export PVSNESLIB_MINOR := 3 +export PVSNESLIB_PATCH := 0 export PVSNESLIB_VERSION := $(PVSNESLIB_MAJOR).$(PVSNESLIB_MINOR).$(PVSNESLIB_PATCH) # Directory with docs config and output (via doxygen) @@ -42,7 +42,7 @@ clean: @if [ "$(KEEP_LIB)" -eq 0 ]; then \ rm -rf $(PVSDOCSDIR)/html; \ rm -f pvsneslib_version.txt; \ - rm -f lib/*ROM/*; \ + rm -rf lib; \ fi # Check if Doxygen is installed From c151033e158039e798e0d065fae9af09cac68aef Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Fri, 22 Mar 2024 07:36:30 +0100 Subject: [PATCH 010/106] chore(*): add fastrom / hirom support --- pvsneslib/include/snes.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pvsneslib/include/snes.h b/pvsneslib/include/snes.h index ff5e523c..4c6486ce 100644 --- a/pvsneslib/include/snes.h +++ b/pvsneslib/include/snes.h @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------- - Copyright (C) 2012-2021 + Copyright (C) 2012-2024 This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any @@ -75,6 +75,7 @@ - Neoflash Menu google code. - Devkitpro team for pvsneslib structure (lib, makefile, examples, and so on ...). - undisbeliever for his great platform code example on github. + - digidwrf for fastrom / hirom support. */ // adding the example page. @@ -160,9 +161,10 @@ \example scoring/scoring.c - + \example testregion/testregion.c \example typeconsole/src/pal_ntsc.c + \example memory_mapping/src/memory_mapping.c */ From 82ef6601a6e32381298d6aea3951e288beeb88e5 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Fri, 22 Mar 2024 07:37:10 +0100 Subject: [PATCH 011/106] chore(*): add thanks for fastrom / hirom supprot --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b0b94bf9..7f77095c 100644 --- a/readme.md +++ b/readme.md @@ -97,8 +97,9 @@ Sydney Hunter by [CollectorVision](https://collectorvision.com/store/shop/snes/s - [**Shiru**](http://shiru.untergrund.net/) for snesbmp idea & sound tools. - [**Mukunda**](http://snes.mukunda.com/) for smconv tool. - **RedBug** for constify tcc bug fix and tips for Linux and Docker. -- [**Mills32**](https://github.com/mills32) for his mode7 3D example. +- [**Mills32**](https://github.com/mills32/) for his mode7 3D example. - [**N_Arno**](https://github.com/nArnoSNES/) for his help on Linux version. +- [**DigiDwrf**](https://github.com/DigiDwrf/) for hirom / fastrom support. And, of course, all the [**discord community**](https://discord.gg/DzEFnhB) ! From d5ad8e414b120b72e13705328f1129820fc2a365 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sat, 23 Mar 2024 09:02:12 +0100 Subject: [PATCH 012/106] chore(*): change version number --- tools/smconv/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/smconv/Makefile b/tools/smconv/Makefile index 8a1e6c6c..610e09aa 100644 --- a/tools/smconv/Makefile +++ b/tools/smconv/Makefile @@ -1,5 +1,5 @@ # Define variables -VERSION := 2.0.0 +VERSION := 2.1.0 DATESTRING := $(shell date +%Y%m%d) # Define compilers and flags From c78c29e7ce0c0f35159d4bd5a71e1681ff24cee9 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sat, 23 Mar 2024 09:03:57 +0100 Subject: [PATCH 013/106] chore(*): change console messages --- tools/smconv/convert.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tools/smconv/convert.cpp b/tools/smconv/convert.cpp index 73735584..bd1a9759 100644 --- a/tools/smconv/convert.cpp +++ b/tools/smconv/convert.cpp @@ -12,6 +12,11 @@ #include "it2spc.h" // DEFINES +#define ERRORRED(STRING) "\x1B[31m" STRING "\033[0m" +#define ERRORPINK(STRING) "\x1B[35m" STRING "\033[0m" +#define ERRORBRIGHT(STRING) "\x1B[97m" STRING "\033[0m" + + #define SMCONVVERSION __BUILD_VERSION #define SMCONVDATE __BUILD_DATE @@ -37,14 +42,14 @@ const char USAGE[] = { "\n-v Show version" "\n\nTips:" "\nTypical options to create soundbank for project:" - "\n smconv -s -o build/soundbank -h input1.it input2.it" + "\n smconv -s -o build/soundbank input1.it input2.it" "\n\nAnd for IT->SPC:" "\n smconv input.it" "\n\nUse -V to view how much RAM the modules will use.\n"}; const char VERSION[] = { "smconv (" SMCONVDATE ") version " SMCONVVERSION "" - "\nCopyright (c) 2012-2021 Alekmaul " + "\nCopyright (c) 2012-2024 Alekmaul " "\nBased on SNESMOD (C) 2009 Mukunda Johnson (www.mukunda.com)\n"}; std::string PATH; @@ -73,24 +78,24 @@ int main(int argc, char *argv[]) if (argc < 2) { printf(USAGE); - exit(1); + exit (EXIT_FAILURE); } if (od.output.empty()) { - printf("\nsmconv: error 'Missing output file'\n"); - exit(1); + printf ("%s: " ERRORRED("fatal error") ": missing output file\n", ERRORBRIGHT("smconv")); + exit (EXIT_FAILURE); } if (od.files.empty()) { - printf("\nsmconv: error 'Missing input file'\n"); - return 0; + printf ("%s: " ERRORRED("fatal error") ": missing input file\n", ERRORBRIGHT("smconv")); + exit (EXIT_FAILURE); } if (VERBOSE) { - printf("\nsmconv: 'Loading modules...'"); + printf ("%s: Loading modules...\n", ERRORBRIGHT("smconv")); fflush(stdout); } @@ -98,12 +103,13 @@ int main(int argc, char *argv[]) if (VERBOSE) { - printf("\nsmconv: 'Starting conversion...'"); + printf ("%s: Starting conversion...\n", ERRORBRIGHT("smconv")); fflush(stdout); } IT2SPC::Bank result(bank, od.hirom, od.check_effect_size); + // export products if (od.spc_mode) { From fd2645a8ac96936bbcb5e6773d0437c328f6a83b Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sat, 23 Mar 2024 09:04:50 +0100 Subject: [PATCH 014/106] chore(*): change console messages --- tools/smconv/inputdata.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tools/smconv/inputdata.cpp b/tools/smconv/inputdata.cpp index 9b43a23c..9b6e9356 100644 --- a/tools/smconv/inputdata.cpp +++ b/tools/smconv/inputdata.cpp @@ -2,6 +2,10 @@ extern std::string PATH; +#define ERRORRED(STRING) "\x1B[31m" STRING "\033[0m" +#define ERRORPINK(STRING) "\x1B[35m" STRING "\033[0m" +#define ERRORBRIGHT(STRING) "\x1B[97m" STRING "\033[0m" + namespace ConversionInput { @@ -92,7 +96,7 @@ namespace ConversionInput arg++; if (arg == argc) { - printf("\nsmconv: error 'No output file specified'"); + printf ("%s: " ERRORRED("fatal error") ": No output file specified\n", ERRORBRIGHT("smconv")); return; } output = argv[arg]; @@ -122,7 +126,7 @@ namespace ConversionInput arg++; if (arg == argc) { - printf("\nsmconv: error 'No bank number specified'"); + printf ("%s: " ERRORRED("fatal error") ": No bank number specified\n", ERRORBRIGHT("smconv")); return; } if (isdigit(argv[arg][0])) @@ -131,7 +135,7 @@ namespace ConversionInput } else { - printf("\nsmconv: error 'Incorrect bank number'"); + printf ("%s: " ERRORRED("fatal error") ": Incorrect bank number\n", ERRORBRIGHT("smconv")); return; } } @@ -139,7 +143,12 @@ namespace ConversionInput break; case ARG_INPUT: // input - + if (FILE *file = fopen(argv[arg], "r")) { + fclose(file); + } + else { + return; + } files.push_back(argv[arg]); arg++; break; From d3f4d67d22cc8355f1f074a6b2107ed10e517dc1 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sat, 23 Mar 2024 09:05:52 +0100 Subject: [PATCH 015/106] chore(*): change console message and add a message when more than 8 channels --- tools/smconv/it2spc.cpp | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/tools/smconv/it2spc.cpp b/tools/smconv/it2spc.cpp index d84ecb61..61e16f71 100644 --- a/tools/smconv/it2spc.cpp +++ b/tools/smconv/it2spc.cpp @@ -6,6 +6,10 @@ #include "spc_program.h" +#define ERRORRED(STRING) "\x1B[31m" STRING "\033[0m" +#define ERRORPINK(STRING) "\x1B[35m" STRING "\033[0m" +#define ERRORBRIGHT(STRING) "\x1B[97m" STRING "\033[0m" + u8 CurrentInstrument = 0; u8 patch_byte = 0x3C; u8 max_instruments = 64; // changing these values breaks @@ -62,9 +66,10 @@ namespace IT2SPC if (VERBOSE) { - printf("\n-----------------------------------------------------------------------"); - printf("\n Total Modules Size: [%6i bytes]", totabanksize); - printf("\n Total IT Size: [%6i bytes]", totalitsize); + printf("-----------------------------------------------------------------------\n"); + printf(" Total Modules Size: [%6i bytes]\n", totabanksize); + printf(" Total IT Size: [%6i bytes]\n", totalitsize); + fflush(stdout); } } @@ -80,9 +85,10 @@ namespace IT2SPC if (VERBOSE) { - printf("\n-----------------------------------------------------------------------"); - printf("\nAdding module, Title: <%s>", mod.Title); - printf("\n IT Size: [%5i bytes]", size); + printf("-----------------------------------------------------------------------\n"); + printf("Adding module, Title: <%s>\n", mod.Title); + printf(" IT Size: [%5i bytes]\n", size); + fflush(stdout); } totalitsize += size; @@ -208,7 +214,6 @@ namespace IT2SPC const std::vector &directory, const std::vector &sources) { - id = Path2ID("MOD_", mod.Filename); // id = cinput.id; @@ -240,8 +245,10 @@ namespace IT2SPC for (int i = 0; i < max_length; i++) Sequence[i] = i < mod.Length ? mod.Orders[i] : 255; + //printf("mod.PatternCount %d\n",mod.PatternCount);fflush(stdout); for (int i = 0; i < mod.PatternCount; i++) { + //printf("Patterns.push_back 2 %d %08x\n",i,*(mod.Patterns[i]));fflush(stdout); Patterns.push_back(new Pattern(*(mod.Patterns[i]))); } @@ -291,7 +298,7 @@ namespace IT2SPC if (ChkSfx && (totalsizem1 != 0)) { printf( - "\nConversion report:\n" + "Conversion report:\n" " Pattern data: [%5i bytes] Module Length: [%i/%i]\n" " Sample data: [%5i bytes] Patterns: [%i/%i]\n" " Instrument data: [%5i bytes] Instruments: [%i/%i]\n" @@ -308,7 +315,7 @@ namespace IT2SPC else { printf( - "\nConversion report:\n" + "Conversion report:\n" " Pattern data: [%5i bytes] Module Length: [%i/%i]\n" " Sample data: [%5i bytes] Patterns: [%i/%i]\n" " Instrument data: [%5i bytes] Instruments: [%i/%i]\n" @@ -327,7 +334,7 @@ namespace IT2SPC if (totalsize > spc_ram_size) { - printf("\nsmconv: error 'Module is too big. Maximum is %i bytes'\n", spc_ram_size); + printf ("%s: " ERRORRED("error") ": Module is too big. Maximum is %i bytes\n", ERRORBRIGHT("smconv"),spc_ram_size); } } } @@ -586,11 +593,15 @@ namespace IT2SPC int channel = (chvar - 1) & 63; data_bits |= 1 << channel; + if (channel>7) { + printf ("%s: " ERRORRED("error") ": More than 8 channels. Found channel %i\n", ERRORBRIGHT("smconv"),channel+1); + return; + } u8 maskvar; if (chvar & 128) { - maskvar = *read++; + maskvar = *read++; maskvar = ((maskvar >> 4) | (maskvar << 4)) & 0xFF; maskvar |= maskvar >> 4; p_maskvar[channel] = maskvar; @@ -602,7 +613,7 @@ namespace IT2SPC row_buffer.push_back(maskvar); - if (maskvar & 16) + if (maskvar & 16) { // note row_buffer.push_back(*read++); } @@ -618,7 +629,6 @@ namespace IT2SPC u8 cmd, param; if (maskvar & 128) { // cmd+param - row_buffer.push_back(cmd = *read++); row_buffer.push_back(param = *read++); } From 91bc91e71f3112623673b814449009ffd589c88e Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sat, 23 Mar 2024 09:07:14 +0100 Subject: [PATCH 016/106] chore(*): change console messages --- tools/smconv/itloader.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tools/smconv/itloader.cpp b/tools/smconv/itloader.cpp index 5e5fbf28..8d861318 100644 --- a/tools/smconv/itloader.cpp +++ b/tools/smconv/itloader.cpp @@ -1,6 +1,12 @@ #include "itloader.h" #include "io.h" +// IT file format found here : https://fileformats.fandom.com/wiki/Impulse_tracker + +#define ERRORRED(STRING) "\x1B[31m" STRING "\033[0m" +#define ERRORPINK(STRING) "\x1B[35m" STRING "\033[0m" +#define ERRORBRIGHT(STRING) "\x1B[97m" STRING "\033[0m" + namespace ITLoader { template static void deletePtrVector( std::vector &vecs ) { @@ -299,6 +305,7 @@ namespace ITLoader { } } } else { + printf ("%s: " ERRORRED("error") ": unsupported compressed samples\n", ERRORBRIGHT("smconv")); // TODO : accept compressed samples. } } @@ -389,10 +396,7 @@ namespace ITLoader { // check compression code (1 = PCM) if( file.Read16() != 1 ) { - //if( verbose ) - // printf( "Unsupported WAV format.\n" ); - //return LOADWAV_UNKNOWN_COMP; - printf("\nsmconv: error 'Unsupported WAV format'\n" ); + printf ("%s: " ERRORRED("fatal error") ": Unsupported WAV format\n", ERRORBRIGHT("smconv")); return; } @@ -410,9 +414,7 @@ namespace ITLoader { bit_depth = file.Read16(); if( bit_depth != 8 && bit_depth != 16 ) { - //if( verbose ) - // printf( "Unsupported bit-depth.\n" ); - printf("\nsmconv: error 'Unsupported WAV bit depth'\n"); + printf ("%s: " ERRORRED("fatal error") ": Unsupported WAV bit depth\n", ERRORBRIGHT("smconv")); return;// LOADWAV_UNSUPPORTED_BD; } @@ -442,7 +444,7 @@ namespace ITLoader { if( !hasformat ) { - printf("\nsmconv: error 'CORRUPT WAV FILE...'\n"); + printf ("%s: " ERRORRED("fatal error") ": CORRUPT WAV FILE...\n", ERRORBRIGHT("smconv")); //printf("\nsmconv: error 'CORRUPT WAV FILE...'\n"); return;// LOADWAV_CORRUPT; } From a6c9e8d22f63c94cf8b34763758588de30ef8f7e Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sat, 23 Mar 2024 09:49:38 +0100 Subject: [PATCH 017/106] chore(*): add sound and music initial page --- wiki/Sounds.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++- wiki/_Sidebar.md | 1 + 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/wiki/Sounds.md b/wiki/Sounds.md index de08ad5a..459d39cc 100644 --- a/wiki/Sounds.md +++ b/wiki/Sounds.md @@ -1 +1,63 @@ -**to do ** \ No newline at end of file +PVSneslib uses IT files to play musics + +## Tools to create IT files + +You can use a tool like [openMPT](https://openmpt.org/) to create your song. + +## Composition rules for your IT files + +Impulse tracker format must be used. 8 Channels MAX! +Notes can not exceed 128Khz playback rate! +The module must be in instrument mode. It must have instruments! + +DO NOT USE UNDEFINED INSTRUMENTS, they will not silence the channel, THEY WILL CAUSE MALFUNCTION! + +The 64 or less samples must fit within 58K of memory. This is AFTER "BRR" compression. 8-bit samples will be reduced to 9/16 size. 16-bit samples will be reduced to 9/32 size. + +## Tips from KungFuFurby about samples in IT files + +When something going wrong for the songs you attempted to convert: + +- Sample loop points must be divisible by 16. Loop points not divisible by 16 may not convert correctly. +If you don't use loop points divisible by 16, at least make sure the loop length is divisible by 16. + +I use Schism Tracker to perform the job of loop point analysis and loop point adjustments since I can just simply type in the numbers. + +I use a calculator to take care of loop point problems simply related to the sample being at the wrong sample rate to have loop point lengths divisible by 16 (the length of the looping portion of the sample should at the very least be divisible by 16). +You usually perform this on samples with shorter loop point lengths. + +I don't think it works so well on ones with longer loop point lengths, mainly because by then you're probably not dealing with simple waveforms as loops. + +Using the Bass & Lead sample as an example... + +Loop point is currently defined as... +Start: 3213 +End: 3382 + +That's a loop length of 169. + +I like using powers of 2 for my loop points so that if I have to decrease the quality of the sample, then I can do so as painlessly as possible to the sample (unless I find the degraded quality to be a bad idea), so that means 128 is the value I use here. + +Divide 169 by 128 gets you an unusual decimal number... copy that number. + +Now get the full length of the sample (that's 3383 for this sample) and divide by that decimal number you acquired earlier (169/128). +You'll most likely get another unusual decimal number. Round that off and there's your new length that you will resize the sample to. + +I use Alt-E in Schism Tracker to perform the resize sample command. +The program will ask for the new size of the sample. + +Now you should have a loop length that is divisible by 16. You can perfect the loop point by adjusting them so that the loop point themselves are divisible by 16. + +- Only one sample can be defined per instrument... + +You'd have to duplicate the instruments and then enter the sample ID for all of those notes... and then you have to redefine the instrument IDs depending on the pitch from the old note table. Yeah... + +- I thought in one case, I saw the pitch go too high (it went over 128khz). That's because I noticed a hiccup in the notation. + +For the one song that has this problem, I usually resize the sample and make it half of its length... +however, I may have to make additional adjustments depending on how the loop points are holding up (length may or may not be involved, although usually I'm checking the loop points themselves to make sure that they are divisible by 32 or some higher power of 16... this indicates how many times you can cut the sample in half). + +- Note Cut is the only New Note Action supported for SNESMod. + +One of these songs is the most visibly affected by this problem, and that's because SNESMod doesn't virtually allocate channels. +You have to modify the patterns so that the note off commands go where the note would originally play, and the new note is put on another channel. \ No newline at end of file diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md index bb42ecd8..437411c4 100644 --- a/wiki/_Sidebar.md +++ b/wiki/_Sidebar.md @@ -12,6 +12,7 @@ 1. **[Sprites](https://github.com/alekmaul/pvsneslib/wiki/Sprites)** 1. **[Dynamic Sprites](https://github.com/alekmaul/pvsneslib/wiki/Dynamic-Sprites)** 1. **[Backgrounds](https://github.com/alekmaul/pvsneslib/wiki/Backgrounds)** + 1. **[Sounds and Musics](https://github.com/alekmaul/pvsneslib/wiki/Sounds)** 1. **[Map Engine 1: Import maps for Tiled](https://github.com/alekmaul/pvsneslib/wiki/Import-maps-for-Tiled)** 1. **[Map Engine 2: Update maps in Tiled](https://github.com/alekmaul/pvsneslib/wiki/Update-Maps-in-Tiled)** 1. **[Map Engine 3: Display map on SNES (I)]( https://github.com/alekmaul/pvsneslib/wiki/Display-map-on-SNES-(1))** From 340b5a27f63ff5c66d73550c28c02329b98595ee Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sat, 23 Mar 2024 11:44:51 +0100 Subject: [PATCH 018/106] chore(*): change version --- devkitsnes/readme.txt | 4 ++++ pvsneslib/include/snes/libversion.h | 6 +++--- pvsneslib/pvsneslib_license.txt | 3 +++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/devkitsnes/readme.txt b/devkitsnes/readme.txt index b2a09fbe..abae1cd2 100644 --- a/devkitsnes/readme.txt +++ b/devkitsnes/readme.txt @@ -26,9 +26,13 @@ SPECIAL THANKS n_Arno for his help on Linux version (https://github.com/nArnoSNES/) kobenairb for python & docker optimizations jeffythedragonslayer for code cleaning and new functions + DigiDwrf for hirom / fastrom support CHANGE LOG ---------------------- +VERSION V4.3.0 (March,24,2024) +- See github changelog of the release + VERSION V4.2.1 (March,04,2024) - See github changelog of the release diff --git a/pvsneslib/include/snes/libversion.h b/pvsneslib/include/snes/libversion.h index 6f4c02d5..f7262249 100644 --- a/pvsneslib/include/snes/libversion.h +++ b/pvsneslib/include/snes/libversion.h @@ -2,9 +2,9 @@ #define __PVSNESLIBVERSION_H__ #define _PVSNESLIB_MAJOR_ 4 -#define _PVSNESLIB_MINOR_ 1 -#define _PVSNESLIB_PATCH_ 1 +#define _PVSNESLIB_MINOR_ 3 +#define _PVSNESLIB_PATCH_ 0 -#define _PVSNESLIB_STRING_ "PVSnesLib V4.1.1" +#define _PVSNESLIB_STRING_ "PVSnesLib V4.3.0" #endif // __PVSNESLIBVERSION_H__ diff --git a/pvsneslib/pvsneslib_license.txt b/pvsneslib/pvsneslib_license.txt index 58d1d814..2030ef8b 100644 --- a/pvsneslib/pvsneslib_license.txt +++ b/pvsneslib/pvsneslib_license.txt @@ -21,6 +21,9 @@ -------------------------------------------------------------------------- CHANGE LOG -------------------------------------------------------------------------- +V4.3.0 (March,24,2024) +- See github changelog of the release + V4.2.1 (March,04,2024) - See github changelog of the release From 63447fe9df0d86946a5de906e97dcf04cd6e90d6 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 24 Mar 2024 06:13:33 +0100 Subject: [PATCH 019/106] chore(*): fix some typo --- wiki/Sounds.md | 63 ------------------------------------------------ wiki/_Sidebar.md | 2 +- 2 files changed, 1 insertion(+), 64 deletions(-) delete mode 100644 wiki/Sounds.md diff --git a/wiki/Sounds.md b/wiki/Sounds.md deleted file mode 100644 index 459d39cc..00000000 --- a/wiki/Sounds.md +++ /dev/null @@ -1,63 +0,0 @@ -PVSneslib uses IT files to play musics - -## Tools to create IT files - -You can use a tool like [openMPT](https://openmpt.org/) to create your song. - -## Composition rules for your IT files - -Impulse tracker format must be used. 8 Channels MAX! -Notes can not exceed 128Khz playback rate! -The module must be in instrument mode. It must have instruments! - -DO NOT USE UNDEFINED INSTRUMENTS, they will not silence the channel, THEY WILL CAUSE MALFUNCTION! - -The 64 or less samples must fit within 58K of memory. This is AFTER "BRR" compression. 8-bit samples will be reduced to 9/16 size. 16-bit samples will be reduced to 9/32 size. - -## Tips from KungFuFurby about samples in IT files - -When something going wrong for the songs you attempted to convert: - -- Sample loop points must be divisible by 16. Loop points not divisible by 16 may not convert correctly. -If you don't use loop points divisible by 16, at least make sure the loop length is divisible by 16. - -I use Schism Tracker to perform the job of loop point analysis and loop point adjustments since I can just simply type in the numbers. - -I use a calculator to take care of loop point problems simply related to the sample being at the wrong sample rate to have loop point lengths divisible by 16 (the length of the looping portion of the sample should at the very least be divisible by 16). -You usually perform this on samples with shorter loop point lengths. - -I don't think it works so well on ones with longer loop point lengths, mainly because by then you're probably not dealing with simple waveforms as loops. - -Using the Bass & Lead sample as an example... - -Loop point is currently defined as... -Start: 3213 -End: 3382 - -That's a loop length of 169. - -I like using powers of 2 for my loop points so that if I have to decrease the quality of the sample, then I can do so as painlessly as possible to the sample (unless I find the degraded quality to be a bad idea), so that means 128 is the value I use here. - -Divide 169 by 128 gets you an unusual decimal number... copy that number. - -Now get the full length of the sample (that's 3383 for this sample) and divide by that decimal number you acquired earlier (169/128). -You'll most likely get another unusual decimal number. Round that off and there's your new length that you will resize the sample to. - -I use Alt-E in Schism Tracker to perform the resize sample command. -The program will ask for the new size of the sample. - -Now you should have a loop length that is divisible by 16. You can perfect the loop point by adjusting them so that the loop point themselves are divisible by 16. - -- Only one sample can be defined per instrument... - -You'd have to duplicate the instruments and then enter the sample ID for all of those notes... and then you have to redefine the instrument IDs depending on the pitch from the old note table. Yeah... - -- I thought in one case, I saw the pitch go too high (it went over 128khz). That's because I noticed a hiccup in the notation. - -For the one song that has this problem, I usually resize the sample and make it half of its length... -however, I may have to make additional adjustments depending on how the loop points are holding up (length may or may not be involved, although usually I'm checking the loop points themselves to make sure that they are divisible by 32 or some higher power of 16... this indicates how many times you can cut the sample in half). - -- Note Cut is the only New Note Action supported for SNESMod. - -One of these songs is the most visibly affected by this problem, and that's because SNESMod doesn't virtually allocate channels. -You have to modify the patterns so that the note off commands go where the note would originally play, and the new note is put on another channel. \ No newline at end of file diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md index 437411c4..56c1de58 100644 --- a/wiki/_Sidebar.md +++ b/wiki/_Sidebar.md @@ -12,7 +12,7 @@ 1. **[Sprites](https://github.com/alekmaul/pvsneslib/wiki/Sprites)** 1. **[Dynamic Sprites](https://github.com/alekmaul/pvsneslib/wiki/Dynamic-Sprites)** 1. **[Backgrounds](https://github.com/alekmaul/pvsneslib/wiki/Backgrounds)** - 1. **[Sounds and Musics](https://github.com/alekmaul/pvsneslib/wiki/Sounds)** + 1. **[Sounds and Musics](https://github.com/alekmaul/pvsneslib/wiki/Sounds-and-Musics)** 1. **[Map Engine 1: Import maps for Tiled](https://github.com/alekmaul/pvsneslib/wiki/Import-maps-for-Tiled)** 1. **[Map Engine 2: Update maps in Tiled](https://github.com/alekmaul/pvsneslib/wiki/Update-Maps-in-Tiled)** 1. **[Map Engine 3: Display map on SNES (I)]( https://github.com/alekmaul/pvsneslib/wiki/Display-map-on-SNES-(1))** From de174d676b9c420ecb8a49f9b69df631ecc499e9 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 24 Mar 2024 06:16:00 +0100 Subject: [PATCH 020/106] feat(*): initial commit --- wiki/Sounds-and-Musics.md | 63 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 wiki/Sounds-and-Musics.md diff --git a/wiki/Sounds-and-Musics.md b/wiki/Sounds-and-Musics.md new file mode 100644 index 00000000..49c19887 --- /dev/null +++ b/wiki/Sounds-and-Musics.md @@ -0,0 +1,63 @@ +PVSneslib uses IT files to play musics, it also uses a specific IT file for sound effects. + +## Tools to create IT files + +You can use a tool like [openMPT](https://openmpt.org/) to create your song. + +## Composition rules for your IT files + +Impulse tracker format must be used. 8 Channels MAX! +Notes can not exceed 128Khz playback rate! +The module must be in instrument mode. It must have instruments! + +DO NOT USE UNDEFINED INSTRUMENTS, they will not silence the channel, THEY WILL CAUSE MALFUNCTION! + +The 64 or less samples must fit within 58K of memory. This is AFTER "BRR" compression. 8-bit samples will be reduced to 9/16 size. 16-bit samples will be reduced to 9/32 size. + +## Tips from KungFuFurby about samples in IT files + +When something going wrong for the songs you attempted to convert: + +- Sample loop points must be divisible by 16. Loop points not divisible by 16 may not convert correctly. +If you don't use loop points divisible by 16, at least make sure the loop length is divisible by 16. + +I use Schism Tracker to perform the job of loop point analysis and loop point adjustments since I can just simply type in the numbers. + +I use a calculator to take care of loop point problems simply related to the sample being at the wrong sample rate to have loop point lengths divisible by 16 (the length of the looping portion of the sample should at the very least be divisible by 16). +You usually perform this on samples with shorter loop point lengths. + +I don't think it works so well on ones with longer loop point lengths, mainly because by then you're probably not dealing with simple waveforms as loops. + +Using the Bass & Lead sample as an example... + +Loop point is currently defined as... +Start: 3213 +End: 3382 + +That's a loop length of 169. + +I like using powers of 2 for my loop points so that if I have to decrease the quality of the sample, then I can do so as painlessly as possible to the sample (unless I find the degraded quality to be a bad idea), so that means 128 is the value I use here. + +Divide 169 by 128 gets you an unusual decimal number... copy that number. + +Now get the full length of the sample (that's 3383 for this sample) and divide by that decimal number you acquired earlier (169/128). +You'll most likely get another unusual decimal number. Round that off and there's your new length that you will resize the sample to. + +I use Alt-E in Schism Tracker to perform the resize sample command. +The program will ask for the new size of the sample. + +Now you should have a loop length that is divisible by 16. You can perfect the loop point by adjusting them so that the loop point themselves are divisible by 16. + +- Only one sample can be defined per instrument... + +You'd have to duplicate the instruments and then enter the sample ID for all of those notes... and then you have to redefine the instrument IDs depending on the pitch from the old note table. Yeah... + +- I thought in one case, I saw the pitch go too high (it went over 128khz). That's because I noticed a hiccup in the notation. + +For the one song that has this problem, I usually resize the sample and make it half of its length... +however, I may have to make additional adjustments depending on how the loop points are holding up (length may or may not be involved, although usually I'm checking the loop points themselves to make sure that they are divisible by 32 or some higher power of 16... this indicates how many times you can cut the sample in half). + +- Note Cut is the only New Note Action supported for SNESMod. + +One of these songs is the most visibly affected by this problem, and that's because SNESMod doesn't virtually allocate channels. +You have to modify the patterns so that the note off commands go where the note would originally play, and the new note is put on another channel. \ No newline at end of file From 636e8601e869a279c22e379e0485af02fbd859c6 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 24 Mar 2024 06:54:16 +0100 Subject: [PATCH 021/106] feat(*): initial commit --- wiki/HiRom-and-FastRom.md | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 wiki/HiRom-and-FastRom.md diff --git a/wiki/HiRom-and-FastRom.md b/wiki/HiRom-and-FastRom.md new file mode 100644 index 00000000..74ec1fb3 --- /dev/null +++ b/wiki/HiRom-and-FastRom.md @@ -0,0 +1,45 @@ +** to do ** + +There are two main types of SNES cartridges, the SNES community refers to them as LoROM and HiROM cartridges. Both have different memory mapping. + +## HiRom and LoRom + +In LoROM mode, the ROM is always mapped in the upper half of each bank, thus 32 Kilobytes per chunk. + +The banks $00 – $7D (address: $8000 - $FFFF) hold continuous data, as well as banks $80 - $FF. SRAM on the cartridge is mapped continuously and repeatedly - 8 Kilobyte of SRAM are mapped at $0000 - $1FFF, $2000 – $3FFF, $4000 – $5FFF and so on. + +Because the WRAM of the SNES is mapped at bank $7E - $7F, these banks do not map to the last SRAM/ROM chunks. This memory has to be accessed via the banks $80 - $FF. There is no other way of accessing this memory both in LoROM and HiROM mode. + +LoROM was established to make sure that the system banks ($00 – $3F) higher pages (>7) are actually used. This is done by loading the entire ROM only in higher pages and in 32 Kilobyte chunks. 32 KB * $80 banks == 4 Megabyte. + +HiROM is a bit more complex to understand. Unlike LoROM, it does not use $80 (128) banks to map the ROM into the address space of the SNES, but only $40 (64) banks. + +Also unlike LoROM, these banks are used to their full extend, that is, 64 KB per chunk. 64 KB * $40 banks == 4 Megabytes. The banks $40 – $7D (address: $0000 - $FFFF) hold continuous data, as well as banks $C0 - $FF. + +Beware that HiROM also creates mappings for banks $00 – $3F and $80 - $BF. As those are system banks, their lower pages (<8) are already mapped - but the higher pages are free, so that many portions of the ROM are mirrored four times into the address space of the SNES. SRAM on the cartridge is mapped into the banks $20 – $3F, in 8 Kilobyte chunks. + +As there are only 32 banks reserved for this, the possible SRAM amount accessible in HiROM is theoretically lower than in LoROM (256 KB vs. 512 KB). Because the WRAM of the SNES is mapped at bank $7E - $7F, these banks do not map to the last ROM chunks. This memory has to be accessed via the $80 - $FF banks + +Banks $80 - $FF can also be used for faster memory access. Many portions of memory <$80 are accessed at 2.68 MHz (200 ns). Accessing memory >$80 is done at 3.58 MHz (120 ns) if the value at address $420D (hardware register) is set to 1. + +LoROM basically means that the address line A15 is ignored by the cartridge, so the cartridge doesn't distinguish between $0000-$7FFF and $8000-$FFFF in any bank. Smaller ROMs use this model to prevent wasted space in banks $00–$3F. + +PVSneslib is shipped with a full set of lib binaries with 4 different configurations: +- LoROM - SlowROM (already present by default) +- LoROM - FastROM (all sections with .BASE $80) +- HiROM - SlowROM (core sections with .BASE $00 ORG $8000 and everything else with .BASE $40 ORG 0) +- HiROM - FastROM (core sections with .BASE $80 ORG $8000 and everything else with .BASE $C0 ORG 0) + +Notice that BANKS $7E and $7F are for RAM use, so HiROM - SlowROM won't be able to use that banks for storing ROM. +TCC compiler was changed to have some small functions that inyect .BASE directives before **.SECTIONS** and **.RAMSECTIONS** + +TO use HiRom, you must specify it inside your makefile +```bash +export HIROM=1 +``` + +### FastRom + +. + +Some part of this article is from https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map#LoROM. \ No newline at end of file From cb7061ad94586ba0ad21a7ef9309378460eac011 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 24 Mar 2024 06:54:33 +0100 Subject: [PATCH 022/106] feat(*): add lowrom fastrom page --- wiki/_Sidebar.md | 1 + 1 file changed, 1 insertion(+) diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md index 56c1de58..bb3fd454 100644 --- a/wiki/_Sidebar.md +++ b/wiki/_Sidebar.md @@ -13,6 +13,7 @@ 1. **[Dynamic Sprites](https://github.com/alekmaul/pvsneslib/wiki/Dynamic-Sprites)** 1. **[Backgrounds](https://github.com/alekmaul/pvsneslib/wiki/Backgrounds)** 1. **[Sounds and Musics](https://github.com/alekmaul/pvsneslib/wiki/Sounds-and-Musics)** + 1. **[HiROm and FastRom](https://github.com/alekmaul/pvsneslib/wiki/HiRom-and-FastRom)** 1. **[Map Engine 1: Import maps for Tiled](https://github.com/alekmaul/pvsneslib/wiki/Import-maps-for-Tiled)** 1. **[Map Engine 2: Update maps in Tiled](https://github.com/alekmaul/pvsneslib/wiki/Update-Maps-in-Tiled)** 1. **[Map Engine 3: Display map on SNES (I)]( https://github.com/alekmaul/pvsneslib/wiki/Display-map-on-SNES-(1))** From 7803c6505e3239612ef83ff4b63c87820e0a7db1 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 24 Mar 2024 06:54:49 +0100 Subject: [PATCH 023/106] chore(*): continue writing... --- wiki/Sounds-and-Musics.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wiki/Sounds-and-Musics.md b/wiki/Sounds-and-Musics.md index 49c19887..44ac6796 100644 --- a/wiki/Sounds-and-Musics.md +++ b/wiki/Sounds-and-Musics.md @@ -1,3 +1,5 @@ +** to do ** + PVSneslib uses IT files to play musics, it also uses a specific IT file for sound effects. ## Tools to create IT files From c296d69791354f942dc1d2fca890b5f716893116 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 24 Mar 2024 08:16:51 +0100 Subject: [PATCH 024/106] chore(*): continue fixing typo and add explanation --- wiki/HiRom-and-FastRom.md | 38 ++++++++++++++++++++++++++++++++------ wiki/_Sidebar.md | 2 +- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/wiki/HiRom-and-FastRom.md b/wiki/HiRom-and-FastRom.md index 74ec1fb3..e7b8ddf6 100644 --- a/wiki/HiRom-and-FastRom.md +++ b/wiki/HiRom-and-FastRom.md @@ -24,6 +24,16 @@ Banks $80 - $FF can also be used for faster memory access. Many portions of memo LoROM basically means that the address line A15 is ignored by the cartridge, so the cartridge doesn't distinguish between $0000-$7FFF and $8000-$FFFF in any bank. Smaller ROMs use this model to prevent wasted space in banks $00–$3F. +### FastRom + +SNES games are designed to run under SlowROM (2.68 MHz) to FastROM (3.58 MHz). FastROM allows the SNES CPU read data and opcodes from the ROM 33.58% faster compared to SlowROM. + +Depending on the game, FastROM will make the game run about 10%-33% faster compared to the original SlowROM version. + +This depends on how frequent the game accesses the ROM chip, since the other componenets such as WRAM @ 2.68 MHz, PPU @ 3.58 MHz, DMA @ 2.68 MHz and SRAM @ 2.68 MHz will stay at the same speed. + +## HiRom and LoRom with PVSneslib + PVSneslib is shipped with a full set of lib binaries with 4 different configurations: - LoROM - SlowROM (already present by default) - LoROM - FastROM (all sections with .BASE $80) @@ -33,13 +43,29 @@ PVSneslib is shipped with a full set of lib binaries with 4 different configurat Notice that BANKS $7E and $7F are for RAM use, so HiROM - SlowROM won't be able to use that banks for storing ROM. TCC compiler was changed to have some small functions that inyect .BASE directives before **.SECTIONS** and **.RAMSECTIONS** -TO use HiRom, you must specify it inside your makefile +To use HiRom, you must specify it inside your makefile ```bash export HIROM=1 ``` - -### FastRom - -. -Some part of this article is from https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map#LoROM. \ No newline at end of file +To use FastRom, you must specify it inside your makefile +```bash +export FASTROM=1 +``` + +# memory_mapping example to help you with coding + +In this example, all the source code are in the **src** directory. It consists of only one file **memory_mapping.c**. + +We declared that we are using Fast and Hi rom in **Makefile**, just before including **snes_rules**: +```bash +# Tell the compiler to export a Mode 21 (HiROM) memory mapped ROM +HIROM=1 +# Tell the compiler to use FastROM. $4206 reg is enabled on start and reset, nmi and vectors jumps with an ofset of $80 banks +FASTROM=1 + +include ${PVSNESLIB_HOME}/devkitsnes/snes_rules +``` +That's all, the source code is exactly the same as a LoRow/SlowRom source code. + +Some parts of this article are from https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map#LoROM. \ No newline at end of file diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md index bb3fd454..0471d326 100644 --- a/wiki/_Sidebar.md +++ b/wiki/_Sidebar.md @@ -13,7 +13,7 @@ 1. **[Dynamic Sprites](https://github.com/alekmaul/pvsneslib/wiki/Dynamic-Sprites)** 1. **[Backgrounds](https://github.com/alekmaul/pvsneslib/wiki/Backgrounds)** 1. **[Sounds and Musics](https://github.com/alekmaul/pvsneslib/wiki/Sounds-and-Musics)** - 1. **[HiROm and FastRom](https://github.com/alekmaul/pvsneslib/wiki/HiRom-and-FastRom)** + 1. **[HiRom and FastRom](https://github.com/alekmaul/pvsneslib/wiki/HiRom-and-FastRom)** 1. **[Map Engine 1: Import maps for Tiled](https://github.com/alekmaul/pvsneslib/wiki/Import-maps-for-Tiled)** 1. **[Map Engine 2: Update maps in Tiled](https://github.com/alekmaul/pvsneslib/wiki/Update-Maps-in-Tiled)** 1. **[Map Engine 3: Display map on SNES (I)]( https://github.com/alekmaul/pvsneslib/wiki/Display-map-on-SNES-(1))** From 3d94663cde64fad59319d50a432102b366d476fe Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 24 Mar 2024 08:35:16 +0100 Subject: [PATCH 025/106] chore(*): fix typo --- wiki/Compiling-from-sources.md | 2 +- wiki/Compiling-helloworld-example.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/wiki/Compiling-from-sources.md b/wiki/Compiling-from-sources.md index 6bd96b2d..2bc61600 100644 --- a/wiki/Compiling-from-sources.md +++ b/wiki/Compiling-from-sources.md @@ -315,7 +315,7 @@ Once the compilation is done, you can find your compressed release *(zip)* in th You can help use to improve pvsneslib but you need to make all changes on `develop``. -Don't forget to be on this branch before requestin a Pull Request ! +**Don't forget to be on this branch before requesting a Pull Request !** ```bash git checkout develop diff --git a/wiki/Compiling-helloworld-example.md b/wiki/Compiling-helloworld-example.md index 0ddf9947..3d584a94 100644 --- a/wiki/Compiling-helloworld-example.md +++ b/wiki/Compiling-helloworld-example.md @@ -4,7 +4,7 @@ Go to snes-examples/hello_world to have the famous Hello World example. # Tips to help you with coding -In this example, all the souce code are in the **src** directory. It consists of only one file **hello_world.c**. +In this example, all the source code are in the **src** directory. It consists of only one file **hello_world.c**. You can change this behavior by adding a SRC variable in the makefile. From 540b5391930b89c9100b2ba3425da2bb877a5025 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 24 Mar 2024 08:35:45 +0100 Subject: [PATCH 026/106] chore(*): write emulator examples --- wiki/HiRom-and-FastRom.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/wiki/HiRom-and-FastRom.md b/wiki/HiRom-and-FastRom.md index e7b8ddf6..4f22ef40 100644 --- a/wiki/HiRom-and-FastRom.md +++ b/wiki/HiRom-and-FastRom.md @@ -24,7 +24,7 @@ Banks $80 - $FF can also be used for faster memory access. Many portions of memo LoROM basically means that the address line A15 is ignored by the cartridge, so the cartridge doesn't distinguish between $0000-$7FFF and $8000-$FFFF in any bank. Smaller ROMs use this model to prevent wasted space in banks $00–$3F. -### FastRom +## FastRom SNES games are designed to run under SlowROM (2.68 MHz) to FastROM (3.58 MHz). FastROM allows the SNES CPU read data and opcodes from the ROM 33.58% faster compared to SlowROM. @@ -61,11 +61,22 @@ We declared that we are using Fast and Hi rom in **Makefile**, just before inclu ```bash # Tell the compiler to export a Mode 21 (HiROM) memory mapped ROM HIROM=1 -# Tell the compiler to use FastROM. $4206 reg is enabled on start and reset, nmi and vectors jumps with an ofset of $80 banks +# Tell the compiler to use FastROM. +# $4206 reg is enabled on start and reset, nmi and vectors jumps with an ofset of $80 banks FASTROM=1 include ${PVSNESLIB_HOME}/devkitsnes/snes_rules ``` -That's all, the source code is exactly the same as a LoRow/SlowRom source code. +That's all, the source code is exactly the same as a LoRow/SlowRom source code. + + +You can see FastRom and HiRom support in **no$snes** emulator with **F10** key: +![fasthi_1](https://github.com/alekmaul/pvsneslib/assets/2528347/c2ace721-19dc-4a1f-a958-0f17c58ef7d2) + +And also with menu **Windows/Cart Profile**: +![fasthi_2](https://github.com/alekmaul/pvsneslib/assets/2528347/779a6226-4ef1-4577-9bf1-1b3c9b91d976) + +You can see FastRom and HiRom support in **mesen** with menu **Tools/LogWIndows**: +![fasthi_3](https://github.com/alekmaul/pvsneslib/assets/2528347/5442900e-4a13-40de-b8f8-31311662d405) Some parts of this article are from https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map#LoROM. \ No newline at end of file From 20e9f8061aece16b5153d191badcbf61302cf8d2 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 24 Mar 2024 08:38:27 +0100 Subject: [PATCH 027/106] chore*(): fix typo --- wiki/HiRom-and-FastRom.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wiki/HiRom-and-FastRom.md b/wiki/HiRom-and-FastRom.md index 4f22ef40..fa0b2249 100644 --- a/wiki/HiRom-and-FastRom.md +++ b/wiki/HiRom-and-FastRom.md @@ -1,9 +1,9 @@ ** to do ** -There are two main types of SNES cartridges, the SNES community refers to them as LoROM and HiROM cartridges. Both have different memory mapping. - ## HiRom and LoRom +There are two main types of SNES cartridges, the SNES community refers to them as LoROM and HiROM cartridges. Both have different memory mapping. + In LoROM mode, the ROM is always mapped in the upper half of each bank, thus 32 Kilobytes per chunk. The banks $00 – $7D (address: $8000 - $FFFF) hold continuous data, as well as banks $80 - $FF. SRAM on the cartridge is mapped continuously and repeatedly - 8 Kilobyte of SRAM are mapped at $0000 - $1FFF, $2000 – $3FFF, $4000 – $5FFF and so on. @@ -73,10 +73,10 @@ That's all, the source code is exactly the same as a LoRow/SlowRom source code. You can see FastRom and HiRom support in **no$snes** emulator with **F10** key: ![fasthi_1](https://github.com/alekmaul/pvsneslib/assets/2528347/c2ace721-19dc-4a1f-a958-0f17c58ef7d2) -And also with menu **Windows/Cart Profile**: +And also with menu **Windows/Cart Profile**: ![fasthi_2](https://github.com/alekmaul/pvsneslib/assets/2528347/779a6226-4ef1-4577-9bf1-1b3c9b91d976) -You can see FastRom and HiRom support in **mesen** with menu **Tools/LogWIndows**: +You can see FastRom and HiRom support in **mesen** with menu **Tools/LogWIndows**: ![fasthi_3](https://github.com/alekmaul/pvsneslib/assets/2528347/5442900e-4a13-40de-b8f8-31311662d405) Some parts of this article are from https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map#LoROM. \ No newline at end of file From adc316858351a7b897fc5376449f1c62a3ee0243 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 24 Mar 2024 09:25:45 +0100 Subject: [PATCH 028/106] fix(*): change register number --- snes-examples/memory_mapping/Makefile | 2 +- wiki/HiRom-and-FastRom.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/snes-examples/memory_mapping/Makefile b/snes-examples/memory_mapping/Makefile index 9a2d85aa..ff4d6db3 100644 --- a/snes-examples/memory_mapping/Makefile +++ b/snes-examples/memory_mapping/Makefile @@ -4,7 +4,7 @@ endif # Tell the compiler to export a Mode 21 (HiROM) memory mapped ROM HIROM=1 -# Tell the compiler to use FastROM. $4206 reg is enabled on start and reset, nmi and vectors jumps with an ofset of $80 banks +# Tell the compiler to use FastROM. $420D reg is enabled on start and reset, nmi and vectors jumps with an ofset of $80 banks FASTROM=1 include ${PVSNESLIB_HOME}/devkitsnes/snes_rules diff --git a/wiki/HiRom-and-FastRom.md b/wiki/HiRom-and-FastRom.md index fa0b2249..ba1c3807 100644 --- a/wiki/HiRom-and-FastRom.md +++ b/wiki/HiRom-and-FastRom.md @@ -62,7 +62,7 @@ We declared that we are using Fast and Hi rom in **Makefile**, just before inclu # Tell the compiler to export a Mode 21 (HiROM) memory mapped ROM HIROM=1 # Tell the compiler to use FastROM. -# $4206 reg is enabled on start and reset, nmi and vectors jumps with an ofset of $80 banks +# $420D reg is enabled on start and reset, nmi and vectors jumps with an ofset of $80 banks FASTROM=1 include ${PVSNESLIB_HOME}/devkitsnes/snes_rules From 2fe38931864f7358b31ffc5294b8482e0ec6f5ee Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Thu, 28 Mar 2024 05:42:48 +0100 Subject: [PATCH 029/106] chore(*): vblank count passed now in crt0 and 32 bits --- pvsneslib/include/snes/console.h | 2 +- pvsneslib/source/consoles.asm | 8 +------- pvsneslib/source/crt0_snes.asm | 18 ++++++++++++++++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/pvsneslib/include/snes/console.h b/pvsneslib/include/snes/console.h index ad157229..03bac2ed 100644 --- a/pvsneslib/include/snes/console.h +++ b/pvsneslib/include/snes/console.h @@ -45,7 +45,7 @@ extern u8 scr_txt_font_map[0x800]; /*!< \brief tilemap used for text display */ extern u8 scr_txt_dirty; /*!< \brief flag to redraw text during vblank */ -extern u16 snes_vblank_count; /*!< \brief Number of VBL since consoleInit called */ +extern u32 snes_vblank_count; /*!< \brief Number of VBL since consoleInit called */ extern u8 snes_50hz; /*!< \brief 1 if on a PAL/50Hz SNES */ extern u8 snes_fps; /*!< \brief 50 if PAL console (50 Hz) or 60 if NTSC console (60Hz) */ diff --git a/pvsneslib/source/consoles.asm b/pvsneslib/source/consoles.asm index 294493f9..b8139762 100644 --- a/pvsneslib/source/consoles.asm +++ b/pvsneslib/source/consoles.asm @@ -41,8 +41,6 @@ .BASE $00 .RAMSECTION ".reg_cons7e" BANK $7E SLOT RAMSLOT_0 -snes_vblank_count DW ; to count number of vblank - snes_50hz DB ; 1 if PAL console (50 Hz) instead of NTSC (60Hz) snes_fps DB ; 50 if PAL console (50 Hz) or 60 if NTSC console (60Hz) @@ -418,11 +416,7 @@ cvbloam: stz scr_txt_dirty ; no more transfer of text - ; Count frame number -+ rep #$20 - inc.w snes_vblank_count - - plb ++: plb plp rtl diff --git a/pvsneslib/source/crt0_snes.asm b/pvsneslib/source/crt0_snes.asm index 91e8013a..48b7e3c6 100644 --- a/pvsneslib/source/crt0_snes.asm +++ b/pvsneslib/source/crt0_snes.asm @@ -26,9 +26,12 @@ tcc__f3h dsb 2 move_insn dsb 4 ; 3 bytes mvn + 1 byte rts move_backwards_insn dsb 4 ; 3 bytes mvp + 1 byte rts nmi_handler dsb 4 - + tcc__registers_irq dsb 0 tcc__regs_irq dsb 48 + +snes_vblank_count dsb 4 ; 4 bytes to count number of vblank + .ENDS ; sections "globram.data" and "glob.data" can stay here in the file @@ -231,7 +234,15 @@ FVBlank: pea $7e7e plb plb - lda.w #tcc__registers_irq + rep #$20 + + ; Count frame number + lda.w #$1 + inc.w snes_vblank_count + bne + + inc.w snes_vblank_count+2 + ++: lda.w #tcc__registers_irq tad lda.l nmi_handler sta.b tcc__r10 @@ -327,6 +338,9 @@ fast_start: stz.b tcc__r0 stz.b tcc__r1 + stz.w snes_vblank_count + stz.w snes_vblank_count+2 + jsr.l main ; write exit code to $fffd From 481eb7005b87f4928b2bcf4b9ff9f337ed560c29 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sat, 30 Mar 2024 08:40:27 +0100 Subject: [PATCH 030/106] feat(*): add vblank counter and frame per second function --- pvsneslib/include/snes/console.h | 2 +- pvsneslib/include/snes/video.h | 8 ++++++ pvsneslib/source/crt0_snes.asm | 14 +++++----- pvsneslib/source/videos.asm | 45 ++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/pvsneslib/include/snes/console.h b/pvsneslib/include/snes/console.h index 03bac2ed..14621864 100644 --- a/pvsneslib/include/snes/console.h +++ b/pvsneslib/include/snes/console.h @@ -45,7 +45,7 @@ extern u8 scr_txt_font_map[0x800]; /*!< \brief tilemap used for text display */ extern u8 scr_txt_dirty; /*!< \brief flag to redraw text during vblank */ -extern u32 snes_vblank_count; /*!< \brief Number of VBL since consoleInit called */ +extern u16 snes_vblank_count; /*!< \brief Number of VBL since consoleInit called (16 bits longs so reset each 18 minutes in NTSC)*/ extern u8 snes_50hz; /*!< \brief 1 if on a PAL/50Hz SNES */ extern u8 snes_fps; /*!< \brief 50 if PAL console (50 Hz) or 60 if NTSC console (60Hz) */ diff --git a/pvsneslib/include/snes/video.h b/pvsneslib/include/snes/video.h index 084a33c8..1646edf8 100644 --- a/pvsneslib/include/snes/video.h +++ b/pvsneslib/include/snes/video.h @@ -310,6 +310,8 @@ The overflow flags are set (regardless of OBJ enable/disable in 212Ch), at follo extern u8 videoMode; /*!< \brief Current value of REG_TM */ extern u8 videoModeSub; /*!< \brief Current value of REG_TS */ +extern u16 snes_frame_count; /*!< \brief Number of frame per second (need a call to videoGetFrames() once a frame)*/ + /*! \fn setBrightness(u8 level) \brief sets the screens brightness. \param level 15 = full brightness, 0= black @@ -447,4 +449,10 @@ void setMode7Angle(u8 angle); */ void setMode7Scale(u16 xscale, u16 yscale); +/*! \fn getFPScounter(void) + \brief Return number of frames per second. + \return unsigned short of the current frame per second counter +*/ +unsigned short getFPScounter(void); + #endif // SNES_VIDEO_INCLUDE diff --git a/pvsneslib/source/crt0_snes.asm b/pvsneslib/source/crt0_snes.asm index 48b7e3c6..2ee40991 100644 --- a/pvsneslib/source/crt0_snes.asm +++ b/pvsneslib/source/crt0_snes.asm @@ -30,7 +30,10 @@ nmi_handler dsb 4 tcc__registers_irq dsb 0 tcc__regs_irq dsb 48 -snes_vblank_count dsb 4 ; 4 bytes to count number of vblank +snes_vblank_count dsb 2 ; 2 bytes to count number of vblank +snes_vblank_count_svg dsb 2 ; same thing for saving purpose +snes_frame_count dsb 2 ; 2 bytes for frame counter inside loop +snes_frame_count_svg dsb 2 ; same thing for saving purpose .ENDS @@ -237,12 +240,9 @@ FVBlank: rep #$20 ; Count frame number - lda.w #$1 inc.w snes_vblank_count - bne + - inc.w snes_vblank_count+2 -+: lda.w #tcc__registers_irq + lda.w #tcc__registers_irq tad lda.l nmi_handler sta.b tcc__r10 @@ -339,7 +339,9 @@ fast_start: stz.b tcc__r1 stz.w snes_vblank_count - stz.w snes_vblank_count+2 + stz.w snes_vblank_count_svg + stz.w snes_frame_count + stz.w snes_frame_count_svg jsr.l main diff --git a/pvsneslib/source/videos.asm b/pvsneslib/source/videos.asm index 6dea0305..a996b717 100644 --- a/pvsneslib/source/videos.asm +++ b/pvsneslib/source/videos.asm @@ -1093,3 +1093,48 @@ getPaletteColor: rtl .ENDS + +.SECTION ".videos9_text" SUPERFREE + +;--------------------------------------------------------------------------- +; unsigned short getFPScounter(void) +getFPScounter: + php + phb + + sep #$20 + lda #$0 ; bank 0 for counters + pha + plb + + rep #$20 + lda snes_vblank_count + cmp snes_vblank_count_svg ; is svg < current counter, exit (normaly, never occurs) + bcc _gfctr1 + sec + sbc.l snes_vblank_count_svg ; check if we reach fps (50 or 60) + sbc.l snes_fps + bcc _gfctr + + lda snes_vblank_count ; save vblank count + sta snes_vblank_count_svg + + lda snes_frame_count_svg ; init again frame counter + cmp #99 ; no more than 99 fps (don't be mad ;) ) + bcc + + lda #99 + ++: sta snes_frame_count + stz snes_frame_count_svg +_gfctr: + inc.w snes_frame_count_svg ; increment current frame counter + +_gfctr1: + lda snes_frame_count ; return current value + sta.w tcc__r0 + + plb + plp + rtl + +.ENDS From 58e1146a50907176d4ebd03b3f5e96d938b6522b Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Wed, 3 Apr 2024 06:07:00 +0200 Subject: [PATCH 031/106] fix(*): fix parameters order --- pvsneslib/include/snes/sprite.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvsneslib/include/snes/sprite.h b/pvsneslib/include/snes/sprite.h index c89229aa..1eb79160 100644 --- a/pvsneslib/include/snes/sprite.h +++ b/pvsneslib/include/snes/sprite.h @@ -251,8 +251,8 @@ void oamClear(u16 first, u8 numEntries); \param tileSource address of sprites graphics entry \param tileSize size of sprites graphics \param tilePalette address of sprites palette entry - \param tilePaletteNumber palette number (0..8) \param paletteSize size of palette + \param tilePaletteNumber palette number (0..8) \param address address of sprite graphics (8K-word steps) \param oamsize default OAM size (OBJ_SIZE8_L16, OBJ_SIZE8_L32, OBJ_SIZE8_L64, OBJ_SIZE16_L32, OBJ_SIZE16_L64 and OBJ_SIZE32_L64) */ From 7385179a387857f88308af74ef37f846700f2430 Mon Sep 17 00:00:00 2001 From: Carlos O'Connor Date: Mon, 1 Apr 2024 03:12:30 -0500 Subject: [PATCH 032/106] Mouse support --- pvsneslib/include/snes/pad.h | 25 +- pvsneslib/source/consoles.asm | 6 + pvsneslib/source/crt0_snes.asm | 3 + pvsneslib/source/pads.asm | 244 +++++++++++- snes-examples/pads/mouse/Makefile | 30 ++ snes-examples/pads/mouse/buttons.png | Bin 0 -> 5806 bytes snes-examples/pads/mouse/cursor.png | Bin 0 -> 5153 bytes snes-examples/pads/mouse/data.asm | 28 ++ snes-examples/pads/mouse/hdr.asm | 46 +++ snes-examples/pads/mouse/mouse.c | 421 +++++++++++++++++++++ snes-examples/pads/mouse/pvsneslibfont.bmp | Bin 0 -> 3190 bytes 11 files changed, 799 insertions(+), 4 deletions(-) create mode 100644 snes-examples/pads/mouse/Makefile create mode 100644 snes-examples/pads/mouse/buttons.png create mode 100644 snes-examples/pads/mouse/cursor.png create mode 100644 snes-examples/pads/mouse/data.asm create mode 100644 snes-examples/pads/mouse/hdr.asm create mode 100644 snes-examples/pads/mouse/mouse.c create mode 100644 snes-examples/pads/mouse/pvsneslibfont.bmp diff --git a/pvsneslib/include/snes/pad.h b/pvsneslib/include/snes/pad.h index 3b613360..7b53495f 100644 --- a/pvsneslib/include/snes/pad.h +++ b/pvsneslib/include/snes/pad.h @@ -64,6 +64,16 @@ extern u16 pad_keysold[2]; extern u16 pad_keysrepeat[2]; extern u8 snes_mplay5; /*!< \brief 1 if MultiPlay5 connected */ +extern u8 snes_mouse; /*!< \brief 1 if Mouse is going to be used */ + +extern u8 mouseConnect[2]; /*! \brief 1 if Mouse present */ +extern u8 mouseButton[2]; /*! \brief 1 if button is pressed, stays for a bit and then it gets released (Click mode). */ +extern u8 mousePressed[2]; /*! \brief 1 if button is pressed, stays until is unpressed (Turbo mode). */ +extern u8 mouse_x[2], mouse_y[2]; /*! \brief Mouse acceleration. daaaaaaa, d = direction (0: up/left, 1: down/right), a = acceleration. */ +extern u8 mouseSpeedSet[2]; /*! \brief Mouse speed setting. 0: slow, 1: normal, 2: fast */ + +#define mouse_L 0x01 /*! \brief SNES Mouse Left button mask.*/ +#define mouse_R 0x02 /*! \brief SNES Mouse Right button mask.*/ /*! \def REG_JOYxLH @@ -109,7 +119,7 @@ extern u8 snes_mplay5; /*!< \brief 1 if MultiPlay5 connected */ #define REG_JOYxLH(a) (((vuint16 *)0x4218)[(a)]) /*! \fn scanPads() - \brief Wait for pad ready and read pad values in . + \brief Wait for pad ready and read pad values in. */ void scanPads(void); @@ -147,8 +157,19 @@ void padsClear(u16 value); void detectMPlay5(void); /*! \fn scanMPlay5() - \brief Wait for multiplayer5 pads ready and read pad values in . + \brief Wait for multiplayer5 pads ready and read pad values in. */ void scanMPlay5(void); +/*! \fn mouseRead(void) + \brief Wait for mouse ready and read mouse values in. +*/ +void mouseRead(void); + +/*! \fn MouseSpeedChange(u8 port) + \brief Set mouse hardware speed (populate mouseSpeed[] first). + \param port Specify wich port to use (0-1) +*/ +void MouseSpeedChange(u8 port); + #endif // SNES_PADS_INCLUDE diff --git a/pvsneslib/source/consoles.asm b/pvsneslib/source/consoles.asm index b8139762..fe31a0f8 100644 --- a/pvsneslib/source/consoles.asm +++ b/pvsneslib/source/consoles.asm @@ -383,6 +383,12 @@ consoleVblank: beq + jsl scanMPlay5 bra cvbloam ++ lda snes_mouse + beq + + jsl mouseRead + lda mouseConnect + and mouseConnect + 1 ; If both ports have a mouse plugged, it will skip pad controller reading + bne cvbloam + jsl scanPads cvbloam: diff --git a/pvsneslib/source/crt0_snes.asm b/pvsneslib/source/crt0_snes.asm index 2ee40991..84f17684 100644 --- a/pvsneslib/source/crt0_snes.asm +++ b/pvsneslib/source/crt0_snes.asm @@ -343,6 +343,9 @@ fast_start: stz.w snes_frame_count stz.w snes_frame_count_svg + stz.b snes_mouse ; set mouse usage disabled by default + stz.w mouseConnect; + jsr.l main ; write exit code to $fffd diff --git a/pvsneslib/source/pads.asm b/pvsneslib/source/pads.asm index 282d369f..bce18ccd 100644 --- a/pvsneslib/source/pads.asm +++ b/pvsneslib/source/pads.asm @@ -91,6 +91,31 @@ scope_sinceshot dsb 2 .ENDS +;--------------------------------------------------------------------------------- +; Mouse Driver Routine (Ver 1 .00) +;--------------------------------------------------------------------------------- + +.RAMSECTION ".reg_mouse" BANK 0 SLOT 1 + +snes_mouse db ; for lib use. Tells the system to initialize mouse usage +mouseConnect dsb 2 ; Mouse connection ports (D0=4016, D0=4017) + +mouseSpeedSet dsb 2 ; Mouse speed setting +mouse_sp dsb 2 ; Mouse speed + +mouseButton dsb 2 ; Mouse button trigger +mousePressed dsb 2 ; Mouse button turbo + +mouse_y dsb 2 ; Mouse Y direction +mouse_x dsb 2 ; Mouse X direction + +mouse_sb dsb 2 ; Previous switch status + +connect_st dsb 2 + +.ENDS + + .BASE BASE_0 .SECTION ".pads0_text" SUPERFREE @@ -119,7 +144,9 @@ scanPads: lda REG_JOY1L ; read joypad register #1 bit #$0F ; catch non-joypad input beq + ; (bits 0-3 should be zero) - lda.b #$0 + sep #$20 + lda #$0 + rep #$20 +: sta pad_keys ; store 'current' state eor pad_keysold ; compute 'down' state from bits that and pad_keys ; have changed from 0 to 1 @@ -128,7 +155,9 @@ scanPads: lda REG_JOY2L ; read joypad register #2 bit #$0F ; catch non-joypad input beq + ; (bits 0-3 should be zero) - lda.b #$0 + sep #$20 + lda #$0 + rep #$20 +: sta pad_keys+2 ; store 'current' state eor pad_keysold+2 ; compute 'down' state from bits that and pad_keys+2 ; have changed from 0 to 1 @@ -583,4 +612,215 @@ NoScope: plp ; return from input check rts +.ENDS + +;--------------------------------------------------------------------------------- + +;* mouse_read + +;--------------------------------------------------------------------------------- + +;* If this routine is called every frame, then the mouse status will be set +;* to the appropriate registers. +;* INPUT +;* None (Mouse key read automatically) +;* OUTPUT +;* Connection status (mouse_con) D0=1 Mouse connected to Joyl +;* D1=1 Mouse connected to Joy2 +;* Switch (mousePressed,1) D0=left switch turbo +;* D1=right switch turbo +;* Switch (mouseButton,1) D0=left switch trigger +;* D1=right switch trigger +;* Mouse movement (ball) value +;* (mouse_x) D7=0 Positive turn, D7=1 Negative turn +;* D6-D0 X movement value +;* (mouse_y) D7=0 Positive turn, D7=1 Negative turn +;* D6-D0 X movement value + +;--------------------------------------------------------------------------------- + +.SECTION ".mouse_text" SUPERFREE + +;--------------------------------------------------------------------------------- +; void mouseRead(void) +mouseRead: + php + sep #$30 + phb + phx + phy + + lda #$00 ; Set Data Bank to 0 + pha + plb + +_10: + lda REG_HVBJOY + and #$01 + bne _10 ; Automatic read ok? + + ldx #$01 + lda REG_JOY2L ; Joy2 + jsr mouse_data + + lda connect_st+1 + beq _20 + + jsr speed_change + stz connect_st+1 + + bra _30 + +_20: + dex + lda REG_JOY1L ; Joy1 + + jsr mouse_data + + lda connect_st + beq _30 + + jsr speed_change + stz connect_st + +_30: + ply + plx + plb + plp + rtl + +mouse_data: + + sta tcc__r0 ; (421A / 4218 saved to reg0) + and.b #$0F + cmp.b #$01 ; Is the mouse connected? + beq _m10 + + stz mouseConnect,x ; No connection. + + stz mouseButton,x + stz mousePressed,x + stz mouse_x,x + stz mouse_y,x + + rts +_m10: + lda mouseConnect,x ; When mouse is connected, speed will change. + bne _m20 ; Previous connection status + ; (mouse.com judged by lower 1 bit) + lda #$01 ; Connection check flag on + sta mouseConnect,x + sta connect_st,x + rts +_m20: + rep #$10 + ldy #16 ; Read 16 bit data. + sep #$10 +_m30: + lda REG_JOYA,x + + lsr a + rol mouse_x,x + rol mouse_y,x + dey + bne _m30 + + stz mousePressed,x + + rol tcc__r0 + rol mousePressed,x + rol tcc__r0 + rol mousePressed,x ; Switch turbo + + lda mousePressed,x + eor mouse_sb,x ; Get switch trigger + bne _m40 + + stz mouseButton,x + + rts +_m40: + lda mousePressed,x + sta mouseButton,x + sta mouse_sb,x + + rts + +;--------------------------------------------------------------------------------- +; void MouseSpeedChange(u8 port) +MouseSpeedChange: + php + sep #$30 + phb + phx + phy + + lda #$00 ; Set Data Bank to 0 + pha + plb + + lda 8,s ; Set port + tax + + jsr speed_change + + ply + plx + plb + plp + rtl + +speed_change: + php + sep #$30 + + lda mouseConnect,x + beq _s25 + + lda #$10 + sta tcc__r0h +_s10: + lda #$01 + sta REG_JOYA + lda REG_JOYA,x ; Speed change (1 step) + stz REG_JOYA + + lda #$01 ; Read speed data. + sta REG_JOYA ; Shift register clear. + lda #$00 + sta REG_JOYA + + sta mouse_sp,x ; Speed register clear. + + ldy #10 ; Shift register read has no meaning +_s20: + lda REG_JOYA,x + dey + bne _s20 + + lda REG_JOYA,x ; Read speed + + lsr a + rol mouse_sp,x + + lda REG_JOYA, x + + lsr a + rol mouse_sp,x + lda mouse_sp,x + + cmp mouseSpeedSet,x ; Set speed or not? + + beq _s30 + + dec tcc__r0h ; For error check + bne _s10 +_s25: + lda #$80 ; Speed change error. + sta mouse_sp,x +_s30: + plp + rts + .ENDS \ No newline at end of file diff --git a/snes-examples/pads/mouse/Makefile b/snes-examples/pads/mouse/Makefile new file mode 100644 index 00000000..28f6ac0d --- /dev/null +++ b/snes-examples/pads/mouse/Makefile @@ -0,0 +1,30 @@ +ifeq ($(strip $(PVSNESLIB_HOME)),) +$(error "Please create an environment variable PVSNESLIB_HOME by following this guide: https://github.com/alekmaul/pvsneslib/wiki/Installation") +endif + +include ${PVSNESLIB_HOME}/devkitsnes/snes_rules + +.PHONY: bitmaps all + +#--------------------------------------------------------------------------------- +# ROMNAME is used in snes_rules file +export ROMNAME := mouse + +all: bitmaps $(ROMNAME).sfc + +clean: cleanBuildRes cleanRom cleanGfx + +#--------------------------------------------------------------------------------- +pvsneslibfont.pic: pvsneslibfont.bmp + @echo convert font with no tile reduction ... $(notdir $@) + $(GFXCONV) -s 8 -o 2 -u 16 -p -t bmp -i $< + +cursor.pic: cursor.png + @echo convert sprites ... $(notdir $@) + $(GFXCONV) -s 16 -o 48 -u 16 -p -t png -i $< + +buttons.pic: buttons.png + @echo convert graphics ... $(notdir $@) + $(GFXCONV) -s 8 -o 7 -u 16 -p -e 1 -m -t png -i $< + +bitmaps : pvsneslibfont.pic cursor.pic buttons.pic diff --git a/snes-examples/pads/mouse/buttons.png b/snes-examples/pads/mouse/buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..c2be1d02ed8b21ad6714412480f829d5c279a001 GIT binary patch literal 5806 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 literal 0 HcmV?d00001 diff --git a/snes-examples/pads/mouse/data.asm b/snes-examples/pads/mouse/data.asm new file mode 100644 index 00000000..fa89e619 --- /dev/null +++ b/snes-examples/pads/mouse/data.asm @@ -0,0 +1,28 @@ +.include "hdr.asm" + +.section ".rodata1" superfree + +snesfont: +.incbin "pvsneslibfont.pic" + +snespal: +.incbin "pvsneslibfont.pal" + +cursorsprite: +.incbin "cursor.pic" +cursorsprite_end: + +cursorpal: +.incbin "cursor.pal" + +buttonsmap: +.incbin "buttons.map" + +buttonstiles: +.incbin "buttons.pic" +buttonstiles_end: + +buttonspal: +.incbin "buttons.pal" + +.ends diff --git a/snes-examples/pads/mouse/hdr.asm b/snes-examples/pads/mouse/hdr.asm new file mode 100644 index 00000000..9cf154d2 --- /dev/null +++ b/snes-examples/pads/mouse/hdr.asm @@ -0,0 +1,46 @@ +;==LoRom== ; We'll get to HiRom some other time. + +.MEMORYMAP ; Begin describing the system architecture. + SLOTSIZE $8000 ; The slot is $8000 bytes in size. More details on slots later. + DEFAULTSLOT 0 ; There's only 1 slot in SNES, there are more in other consoles. + SLOT 0 $8000 ; Defines Slot 0's starting address. + SLOT 1 $0 $2000 + SLOT 2 $2000 $E000 + SLOT 3 $0 $10000 +.ENDME ; End MemoryMap definition + +.ROMBANKSIZE $8000 ; Every ROM bank is 32 KBytes in size +.ROMBANKS 8 ; 2 Mbits - Tell WLA we want to use 8 ROM Banks + +.SNESHEADER + ID "SNES" ; 1-4 letter string, just leave it as "SNES" + + NAME "LIBSNES MOUSE INPUT " ; Program Title - can't be over 21 bytes, + ; "123456789012345678901" ; use spaces for unused bytes of the name. + + SLOWROM + LOROM + + CARTRIDGETYPE $00 ; $00 = ROM only $02 = ROM+SRAM, see WLA documentation for others + ROMSIZE $08 ; $08 = 2 Mbits, see WLA doc for more.. + SRAMSIZE $00 ; $00 = No Sram, $01 = 16 kbits, see WLA doc for more.. + COUNTRY $01 ; $01 = U.S. $00 = Japan, that's all I know + LICENSEECODE $00 ; Just use $00 + VERSION $00 ; $00 = 1.00, $01 = 1.01, etc. +.ENDSNES + +.SNESNATIVEVECTOR ; Define Native Mode interrupt vector table + COP EmptyHandler + BRK EmptyHandler + ABORT EmptyHandler + NMI VBlank + IRQ EmptyHandler +.ENDNATIVEVECTOR + +.SNESEMUVECTOR ; Define Emulation Mode interrupt vector table + COP EmptyHandler + ABORT EmptyHandler + NMI EmptyHandler + RESET tcc__start ; where execution starts + IRQBRK EmptyHandler +.ENDEMUVECTOR diff --git a/snes-examples/pads/mouse/mouse.c b/snes-examples/pads/mouse/mouse.c new file mode 100644 index 00000000..daddf161 --- /dev/null +++ b/snes-examples/pads/mouse/mouse.c @@ -0,0 +1,421 @@ +/*--------------------------------------------------------------------------------- + + + snes mouse demo + -- alekmaul + + Mouse support by DigiDwrf + + +---------------------------------------------------------------------------------*/ +#include + +#ifndef MOUSE_SPEED +#define MOUSE_SPEED + +#define slow 0 +#define normal 1 +#define fast 2 + +#endif + +extern char snesfont, snespal, cursorsprite, cursorsprite_end, cursorpal, buttonsmap, buttonstiles, buttonstiles_end, buttonspal; +char hex_string[4]; + +// Init some variables +u16 p1_mouse_x = 0x80; +u16 p1_mouse_y = 0x70; +u16 p2_mouse_x = 0x80; +u16 p2_mouse_y = 0x70; + +u8 odd = 0; + +bool mc_mem[2] = {false}; +bool printed[2] = {false}; +bool mouseDown_L[2] = {false}; +bool mouseDown_R[2] = {false}; +bool mouseDown_LR[2] = {false}; +bool speedset[2] = {true}; + +//--------------------------------------------------------------------------------- +int main(void) +{ + snes_mouse = true; // Let's tell the system we're using mouse bios + + // we set mouse speed, or it will just output a random speed. We can change it later manually + mouseSpeedSet[0] = slow; + mouseSpeedSet[1] = slow; + + // Initialize SNES + consoleInit(); + + // Init cursors sprite + oamInitGfxSet(&cursorsprite, (&cursorsprite_end - &cursorsprite), &cursorpal, 48 * 2, 0, 0x0000, OBJ_SIZE16_L32); + + // Initialize text console with our font + consoleSetTextVramBGAdr(0x6800); + consoleSetTextVramAdr(0x3000); + consoleSetTextOffset(0x0100); + consoleInitText(0, 16 * 2, &snesfont, &snespal); + + // Draw a wonderful text :P + consoleDrawText(11, 1, "MOUSE TEST"); + + WaitForVBlank(); // Let's make sure we read mouse for the first time + + if (mouseConnect[0] == false) + consoleDrawText(3, 5, "NO MOUSE PLUGGED ON PORT 0"); + else + { + dmaCopyVram(&buttonsmap + 0x60, 0x6188, 0x0A); // SLOW button pressed + dmaCopyVram(&buttonsmap + 0xA0, 0x61A8, 0x0A); // SLOW button pressed + dmaCopyVram(&buttonsmap + 0x4A, 0x618D, 0x16); // released buttons + dmaCopyVram(&buttonsmap + 0x8A, 0x61AD, 0x16); // released buttons + consoleDrawText(4, 5, "MOUSE PLUGGED ON PORT 0"); + } + + if (mouseConnect[1] == false) + consoleDrawText(3, 17, "NO MOUSE PLUGGED ON PORT 1"); + else + { + dmaCopyVram(&buttonsmap + 0x60, 0x6308, 0x0A); // SLOW button pressed + dmaCopyVram(&buttonsmap + 0xA0, 0x6328, 0x0A); // SLOW button pressed + dmaCopyVram(&buttonsmap + 0x4A, 0x630D, 0x16); // released buttons + dmaCopyVram(&buttonsmap + 0x8A, 0x632D, 0x16); // released buttons + consoleDrawText(4, 17, "MOUSE PLUGGED ON PORT 1"); + } + + // Init background + bgSetGfxPtr(0, 0x2000); + bgSetGfxPtr(1, 0x4000); + bgSetMapPtr(0, 0x6800, SC_32x32); + bgSetMapPtr(1, 0x6000, SC_32x32); + + // Draw buttons for speed change + bgInitTileSet(1, &buttonstiles, &buttonspal, 1, (&buttonstiles_end - &buttonstiles), 16 * 2, BG_16COLORS, 0x4000); + + // Now Put in 16 color mode + setMode(BG_MODE1, 0); + bgSetDisable(2); + + // Wait for nothing :P + setScreenOn(); + + while (1) + { + odd++; + // Optimize Draw text by printing new text just once + if (mouseConnect[0] != mc_mem[0]) + printed[0] = true; + mc_mem[0] = mouseConnect[0]; + + // Update display with current mouse + if (mouseConnect[0] == false) + { + if (printed[0]) + { + WaitForVBlank(); + consoleDrawText(3, 5, "NO MOUSE PLUGGED ON PORT 0"); + consoleDrawText(11, 7, " "); + consoleDrawText(7, 10, " "); + printed[0] = false; + } + oamSetVisible(0, OBJ_HIDE); // Hide 1p cursor + } + else + { + // We transform raw acceleration values into coordinates for OAM. Firt bit tell us mouse direction and the rest tell us how much fast it goes. + if (mouse_x[0] & 0x80) + p1_mouse_x -= mouse_x[0] & 0x7F; + else + p1_mouse_x += mouse_x[0] & 0x7F; + if (mouse_y[0] & 0x80) + p1_mouse_y -= mouse_y[0] & 0x7F; + else + p1_mouse_y += mouse_y[0] & 0x7F; + + // And set some boundaries + if (p1_mouse_x > 0xFF00) + p1_mouse_x = 0; + if (p1_mouse_x > 0xFF) + p1_mouse_x = 0xFF; + if (p1_mouse_y > 0xFF00) + p1_mouse_y = 0; + if (p1_mouse_y > 0xEF) + p1_mouse_y = 0xEF; + + oamSet(0, p1_mouse_x, p1_mouse_y, 3, 0, 0, 0, mouseConnect[1]); + + if (printed[0]) + { + WaitForVBlank(); + consoleDrawText(3, 5, " MOUSE PLUGGED ON PORT 0 "); + consoleDrawText(11, 7, "X: Y:"); + printed[0] = false; + } + + if (mouseConnect[1] == false) + odd = 1; + + if ((odd & 0x01) && (mouseButton[0] == false)) + { + sprintf(hex_string, "%02X", p1_mouse_x); + consoleDrawText(13, 7, hex_string); + sprintf(hex_string, "%02X", p1_mouse_y); + consoleDrawText(19, 7, hex_string); + } + + // mousePressed works as turbo switch, it will be 1 until it gets released, and then it goes back to 0. Good for dragging or writing. + switch (mousePressed[0]) + { + case mouse_L: + if (mouseDown_L[0] == false) + { + consoleDrawText(7, 10, "LEFT BUTTON PRESSED "); + mouseDown_L[0] = true; + mouseDown_R[0] = false; + mouseDown_LR[0] = false; + } + break; + case mouse_R: + if (mouseDown_R[0] == false) + { + consoleDrawText(7, 10, "RIGHT BUTTON PRESSED"); + mouseDown_L[0] = false; + mouseDown_R[0] = true; + mouseDown_LR[0] = false; + } + break; + case mouse_L + mouse_R: + if (mouseDown_LR[0] == false) + { + consoleDrawText(7, 10, "BOTH BUTTONS PRESSED"); + mouseDown_L[0] = false; + mouseDown_R[0] = false; + mouseDown_LR[0] = true; + } + break; + } + } + + // Optimize Draw text by printing new text just once + if (mouseConnect[1] != mc_mem[1]) + printed[1] = true; + mc_mem[1] = mouseConnect[1]; + + if (mouseConnect[1] == false) + { + if (printed[1]) + { + WaitForVBlank(); + consoleDrawText(3, 17, "NO MOUSE PLUGGED ON PORT 1"); + consoleDrawText(11, 19, " "); + consoleDrawText(7, 22, " "); + printed[1] = false; + } + oamSetVisible(4, OBJ_HIDE); // Hide 2p cursor + } + else + { + // We transform raw acceleration values into coordinates for OAM. Firt bit tell us mouse direction and the rest tell us how much fast it goes. + if (mouse_x[1] & 0x80) + p2_mouse_x -= mouse_x[1] & 0x7F; + else + p2_mouse_x += mouse_x[1] & 0x7F; + if (mouse_y[1] & 0x80) + p2_mouse_y -= mouse_y[1] & 0x7F; + else + p2_mouse_y += mouse_y[1] & 0x7F; + + // And set some boundaries + if (p2_mouse_x > 0xFF00) + p2_mouse_x = 0; + if (p2_mouse_x > 0xFF) + p2_mouse_x = 0xFF; + if (p2_mouse_y > 0xFF00) + p2_mouse_y = 0; + if (p2_mouse_y > 0xEF) + p2_mouse_y = 0xEF; + + oamSet(4, p2_mouse_x, p2_mouse_y, 3, 0, 0, 0, mouseConnect[0] << 1); + + if (printed[1]) + { + WaitForVBlank(); + consoleDrawText(3, 17, " MOUSE PLUGGED ON PORT 1 "); + consoleDrawText(11, 19, "X: Y:"); + printed[1] = false; + } + + if (mouseConnect[0] == false) + odd = 0; + + if (((odd & 0x01) == 0) && (mouseButton[1] == false)) + { + sprintf(hex_string, "%02X", p2_mouse_x, p2_mouse_y); + consoleDrawText(13, 19, hex_string); + sprintf(hex_string, "%02X", p2_mouse_y); + consoleDrawText(19, 19, hex_string); + } + + // mousePressed works as turbo switch, it will be 1 until it gets released, and then it goes back to 0. Good for dragging or writing. + switch (mousePressed[1]) + { + case mouse_L: + if (mouseDown_L[1] == false) + { + consoleDrawText(7, 22, "LEFT BUTTON PRESSED "); + mouseDown_L[1] = true; + mouseDown_R[1] = false; + mouseDown_LR[1] = false; + } + break; + case mouse_R: + if (mouseDown_R[1] == false) + { + consoleDrawText(7, 22, "RIGHT BUTTON PRESSED"); + mouseDown_L[1] = false; + mouseDown_R[1] = true; + mouseDown_LR[1] = false; + } + break; + case mouse_L + mouse_R: + if (mouseDown_LR[1] == false) + { + consoleDrawText(7, 22, "BOTH BUTTONS PRESSED"); + mouseDown_L[1] = false; + mouseDown_R[1] = false; + mouseDown_LR[1] = true; + } + break; + } + } + + WaitForVBlank(); + + // mouseButton works as a one frame value, so it gets released shortly after pressing the button. Good for clicking stuff that need to be called once, like buttons. + if (mouseConnect[0]) + { + if (mouseButton[0] & mouse_L) + { + // Let's choose speed setting + if ((p1_mouse_y > 0x5E) && (p1_mouse_y < 0x6C)) + { + if ((p1_mouse_x > 0x44) && (p1_mouse_x < 0x64)) + { + mouseSpeedSet[0] = slow; + speedset[0] = true; + MouseSpeedChange(0); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. + } + if ((p1_mouse_x > 0x6C) && (p1_mouse_x < 0x94)) + { + mouseSpeedSet[0] = normal; + speedset[0] = true; + MouseSpeedChange(0); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. + } + if ((p1_mouse_x > 0x9C) && (p1_mouse_x < 0xBC)) + { + mouseSpeedSet[0] = fast; + speedset[0] = true; + MouseSpeedChange(0); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. + } + } + } + + if (speedset[0]) + { + dmaCopyVram(&buttonsmap + 0x40, 0x6188, 0x20); // released buttons + dmaCopyVram(&buttonsmap + 0x80, 0x61A8, 0x20); // released buttons + + switch (mouseSpeedSet[0]) + { + case slow: + dmaCopyVram(&buttonsmap + 0x60, 0x6188, 0x0A); // SLOW button pressed + dmaCopyVram(&buttonsmap + 0xA0, 0x61A8, 0x0A); // SLOW button pressed + break; + case normal: + dmaCopyVram(&buttonsmap + 0x6A, 0x618D, 0x0C); // NORMAL button pressed + dmaCopyVram(&buttonsmap + 0xAA, 0x61AD, 0x0C); // NORMAL button pressed + break; + case fast: + dmaCopyVram(&buttonsmap + 0x76, 0x6193, 0x0A); // FAST button pressed + dmaCopyVram(&buttonsmap + 0xB6, 0x61B3, 0x0A); // FAST button pressed + break; + } + speedset[0] = false; + } + + if (mousePressed[0] == false) + dmaFillVram(&buttonsmap, 0x6940, 0x40); // wipe text + } + else if (speedset[0] == false) + { + dmaFillVram(&buttonsmap + 0x40, 0x6188, 0x20); // remove buttons + dmaFillVram(&buttonsmap + 0x80, 0x61A8, 0x20); // remove buttons + speedset[0] = true; + } + + if (mouseConnect[1]) + { + if (mouseButton[1] & mouse_L) + { + // Let's choose speed setting + if ((p2_mouse_y > 0xBE) && (p2_mouse_y < 0xCC)) + { + if ((p2_mouse_x > 0x44) && (p2_mouse_x < 0x64)) + { + mouseSpeedSet[1] = slow; + speedset[1] = true; + MouseSpeedChange(1); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. + } + if ((p2_mouse_x > 0x6C) && (p2_mouse_x < 0x94)) + { + mouseSpeedSet[1] = normal; + speedset[1] = true; + MouseSpeedChange(1); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. + } + if ((p2_mouse_x > 0x9C) && (p2_mouse_x < 0xBC)) + { + mouseSpeedSet[1] = fast; + speedset[1] = true; + consoleMesenBreakpoint(); + MouseSpeedChange(1); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. + } + } + } + + if (speedset[1]) + { + dmaCopyVram(&buttonsmap + 0x40, 0x6308, 0x20); // released buttons + dmaCopyVram(&buttonsmap + 0x80, 0x6328, 0x20); // released buttons + + switch (mouseSpeedSet[1]) + { + case slow: + dmaCopyVram(&buttonsmap + 0x60, 0x6308, 0x0A); // SLOW button pressed + dmaCopyVram(&buttonsmap + 0xA0, 0x6328, 0x0A); // SLOW button pressed + break; + case normal: + dmaCopyVram(&buttonsmap + 0x6A, 0x630D, 0x0C); // NORMAL button pressed + dmaCopyVram(&buttonsmap + 0xAA, 0x632D, 0x0C); // NORMAL button pressed + break; + case fast: + dmaCopyVram(&buttonsmap + 0x76, 0x6313, 0x0A); // FAST button pressed + dmaCopyVram(&buttonsmap + 0xB6, 0x6333, 0x0A); // FAST button pressed + break; + } + speedset[1] = false; + } + + if (mousePressed[1] == false) + dmaFillVram(&buttonsmap, 0x6AC0, 0x40); // wipe text + } + else if (speedset[1] == false) + { + dmaFillVram(&buttonsmap + 0x40, 0x6308, 0x20); // remove buttons + dmaFillVram(&buttonsmap + 0x80, 0x6328, 0x20); // remove buttons + speedset[1] = true; + } + } + return 0; +} \ No newline at end of file diff --git a/snes-examples/pads/mouse/pvsneslibfont.bmp b/snes-examples/pads/mouse/pvsneslibfont.bmp new file mode 100644 index 0000000000000000000000000000000000000000..71bc2193705ac527d98aa6fc83cc62ef408bcbf1 GIT binary patch literal 3190 zcmbu9L6+1o5JS@s7A#nFZoxU&bDg>Vp#N1)(o6;xxI3|3mP#te$;CUPU-Dd`EC0kE&t1*^u{{qCYcE?gmJ_$?KlQ z$_9JrS3xf;ullQg&>2%JIna3I2i82qG8;RJ=Qyh3WYo4t>W%7N1IMdCY*H1Tbs%x; zfc?@}bl3}i0PGn6UKbf4OxJoqw%+ENe8W94XS}h@;~Brq`P;h%ctNx-(p8brr8Upe zZV!ZgTsfO#)%1!jzrLbT4*TyYtI4CJNoOCTpJ2ZT!<-8Vcd9>akDa{>V*5B-$_V7)#wPysG{7;_O`738K0+*M0o_{ky%);>PBYpk;fb+^eJUw64 zf(D+o#lv=6@*mG-lBa{h1!1|AyYyKK0L2rrko4sP2O~GPA`+wiTc(n7)*h7AOeEHq z(bb`Q!RTOX zdbzmi;YT+<s%Smm)rWWeSz*^_KmzXZu!@8y3D{tx_+^T5$$)|pJ l^X9k41Sz+TqSL_8aWm9&vEqbp+nMNQIg8|?%Q0uaegh|&OWObd literal 0 HcmV?d00001 From 3f7a4d7f7c656eadb9133cd752616eeb8532850c Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 7 Apr 2024 06:06:30 +0200 Subject: [PATCH 033/106] feat(*): add mouse example --- pvsneslib/include/snes.h | 1 + 1 file changed, 1 insertion(+) diff --git a/pvsneslib/include/snes.h b/pvsneslib/include/snes.h index 4c6486ce..cc29dbb3 100644 --- a/pvsneslib/include/snes.h +++ b/pvsneslib/include/snes.h @@ -122,6 +122,7 @@ \example pads/input/input.c + \example pads/mouse/mouse.c \example pads/multiplay5/multiplay5.c From 024b8e40e172d9405645030342c868cd64ea6c9a Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 7 Apr 2024 06:07:17 +0200 Subject: [PATCH 034/106] chore(*): increase maximum number of objects --- pvsneslib/source/objects.asm | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/pvsneslib/source/objects.asm b/pvsneslib/source/objects.asm index c917861e..ca952227 100644 --- a/pvsneslib/source/objects.asm +++ b/pvsneslib/source/objects.asm @@ -27,8 +27,8 @@ .DEFINE OB_NULL $FFFF ; null value for linked table .DEFINE OB_ID_NULL $0000 ; null value for obj id -.DEFINE OB_MAX 64 ; total number of objects in the game -.DEFINE OB_TYPE_MAX 48 ; total number of type of objects in the game +.DEFINE OB_MAX 96 ; total number of objects in the game +.DEFINE OB_TYPE_MAX 64 ; total number of type of objects in the game .DEFINE OB_SIZE 64 ; 64 bytes for each object @@ -687,7 +687,7 @@ objUpdateAll: pha plb -; stz objneedrefresh ; no global refresh needed + stz objneedrefresh ; no global refresh needed rep #$20 ldx #$0000 @@ -732,17 +732,6 @@ _oiual3y: ; check now y coordinate bcs _oiual32 ; but y is greater than map min _oiual3y1: -; sep #$20 ; check if it was previously on screen to refresh all the objects -; lda objbuffers.1.onscreen,x -; beq _oiual3y11 ; no ? no need to refresh -; stz objbuffers.1.onscreen,x -; lda objneedrefresh ; if we have noticed a global refresh previously, don't do it again -; bne _oiual3y11 -; lda #1 -; sta objneedrefresh -; jsr objOamRefreshAll ; do a global refresh of sprites - -_oiual3y11: jmp _oial4 _oiual32: ; *** now we test if we are really on screen *** From c9adc1ac455186b3810e1418fc99caef6b93027e Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 7 Apr 2024 06:08:09 +0200 Subject: [PATCH 035/106] chore(*): increase max numb of objects and change velocity type --- pvsneslib/include/snes/object.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pvsneslib/include/snes/object.h b/pvsneslib/include/snes/object.h index dfed67cb..6c0b2efb 100644 --- a/pvsneslib/include/snes/object.h +++ b/pvsneslib/include/snes/object.h @@ -38,7 +38,9 @@ #include -#define OB_MAX 64 // total number of objects in the game +#define OB_MAX 96 /*!< total number of objects in the game */ + +#define OB_TYPE_MAX 64 /*!< total number of type of objects in the game */ /*! \struct t_objs \brief object definition (64 bytes) @@ -63,8 +65,8 @@ typedef struct u16 height; /*!< 28 height of the object (from the square 16x16 or 32x32) */ u16 xmin; /*!< 30 min x coordinate for action of object, depends of game (ex: revert direction) */ u16 xmax; /*!< 32 max x coordinate for action of object, depends of game (ex: revert direction) */ - u16 xvel; /*!< 34 x velocity */ - u16 yvel; /*!< 36 y velocity */ + s16 xvel; /*!< 34 x velocity */ + s16 yvel; /*!< 36 y velocity */ u16 tilestand; /*!< 38 tile number object is standing on */ u16 tileabove; /*!< 40 tile number above object */ u16 tilesprop; /*!< 42 tile property stand on */ From 1caf3fd3943a2c5f6d8da6281ce2a3ea7909c79e Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 7 Apr 2024 10:17:44 +0200 Subject: [PATCH 036/106] chore(*): reduce to max 64 objects (too much memory for 96) --- pvsneslib/include/snes/object.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvsneslib/include/snes/object.h b/pvsneslib/include/snes/object.h index 6c0b2efb..3636c277 100644 --- a/pvsneslib/include/snes/object.h +++ b/pvsneslib/include/snes/object.h @@ -38,7 +38,7 @@ #include -#define OB_MAX 96 /*!< total number of objects in the game */ +#define OB_MAX 64 /*!< total number of objects in the game */ #define OB_TYPE_MAX 64 /*!< total number of type of objects in the game */ From 89204e9e7cdba7fb7e267c6d1ff9f1d95e3a2776 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 7 Apr 2024 10:18:04 +0200 Subject: [PATCH 037/106] chore(*): reduce to max 64 objects (too much memory for 96) --- pvsneslib/source/objects.asm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvsneslib/source/objects.asm b/pvsneslib/source/objects.asm index ca952227..ab54b9c0 100644 --- a/pvsneslib/source/objects.asm +++ b/pvsneslib/source/objects.asm @@ -27,7 +27,7 @@ .DEFINE OB_NULL $FFFF ; null value for linked table .DEFINE OB_ID_NULL $0000 ; null value for obj id -.DEFINE OB_MAX 96 ; total number of objects in the game +.DEFINE OB_MAX 64 ; total number of objects in the game .DEFINE OB_TYPE_MAX 64 ; total number of type of objects in the game .DEFINE OB_SIZE 64 ; 64 bytes for each object From d7f26e5b87a79155a6e2a0f9512f28eaca6daec4 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 7 Apr 2024 10:18:43 +0200 Subject: [PATCH 038/106] chore(*): add few more 16pix sprite lut table --- pvsneslib/source/sprites.asm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pvsneslib/source/sprites.asm b/pvsneslib/source/sprites.asm index 643661ce..bf2b39a1 100644 --- a/pvsneslib/source/sprites.asm +++ b/pvsneslib/source/sprites.asm @@ -832,6 +832,10 @@ lkup16oamS: ; lookup table for 16x16 sprites in VRAM (64 sprites max $0000->$10 .word $0800,$0840,$0880,$08c0,$0900,$0940,$0980,$09c0,$0c00,$0c40,$0c80,$0cc0,$0d00,$0d40,$0d80,$0dc0 .word $1000,$1040,$1080,$10c0,$1100,$1140,$1180,$11c0,$1400,$1440,$1480,$14c0,$1500,$1540,$1580,$15c0 .word $1800,$1840,$1880,$18c0,$1900,$1940,$1980,$19c0,$1c00,$1c40,$1c80,$1cc0,$1d00,$1d40,$1d80,$1dc0 + .word $2000,$2040,$2080,$20c0,$2100,$2140,$2180,$21c0,$2400,$2440,$2480,$24c0,$2500,$2540,$2580,$25c0 + .word $2800,$2840,$2880,$28c0,$2900,$2940,$2980,$29c0,$2c00,$2c40,$2c80,$2cc0,$2d00,$2d40,$2d80,$2dc0 + .word $3000,$3040,$3080,$30c0,$3100,$3140,$3180,$31c0,$3400,$3440,$3480,$34c0,$3500,$3540,$3580,$35c0 + .word $3800,$1840,$3880,$38c0,$3900,$3940,$3980,$39c0,$3c00,$3c40,$3c80,$3cc0,$3d00,$3d40,$3d80,$3dc0 lkup16idT0: ; lookup table for 16x16 sprites ID identification when sprite 16 are big sprites .word $0000,$0002,$0004,$0006,$0008,$000A,$000C,$000E,$0020,$0022,$0024,$0026,$0028,$002A,$002C,$002E .word $0040,$0042,$0044,$0046,$0048,$004A,$004C,$004E,$0060,$0062,$0064,$0066,$0068,$006A,$006C,$006E From 983292cd18c77a415787178febbdff3d7fd07b8c Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Mon, 8 Apr 2024 06:01:27 +0200 Subject: [PATCH 039/106] chore(*): add comment for asm files and hirom --- wiki/HiRom-and-FastRom.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/wiki/HiRom-and-FastRom.md b/wiki/HiRom-and-FastRom.md index ba1c3807..1d2b9ba4 100644 --- a/wiki/HiRom-and-FastRom.md +++ b/wiki/HiRom-and-FastRom.md @@ -45,14 +45,27 @@ TCC compiler was changed to have some small functions that inyect .BASE directiv To use HiRom, you must specify it inside your makefile ```bash -export HIROM=1 +HIROM=1 ``` To use FastRom, you must specify it inside your makefile ```bash -export FASTROM=1 +FASTROM=1 +``` + +If you are allocating a RAM variable inside an assembly file, you will need to reset the **.base** to **0** before the **.ramsection** or else the memory will be allocated to the wrong bank. + +For example, without the **.base 0**, the memory could be erroniously allocated to bank **$be** if **.base** is **$40** (SlowRom HiRom) or **$13e** if **.base** is **$c0** (FastRom HiRom). + +```bash +.base 0 +.ramsection "asm_vars" BANK $7e SLOT 2 + asm_variable: dw +.ends ``` +You will need to restore **.base** to **$80/$40/$c0** (depending on the mapping) if you add code or data after the **.ramsection**. + # memory_mapping example to help you with coding In this example, all the source code are in the **src** directory. It consists of only one file **memory_mapping.c**. From 242c543356c5a95599405cefa2de0e6a6838b5c2 Mon Sep 17 00:00:00 2001 From: Carlos O'Connor Date: Mon, 8 Apr 2024 08:11:35 -0500 Subject: [PATCH 040/106] Super Scope support added, some other changes What has been done: - Super Scope code is usable and available now, comes with example mini game. - Some minor changes to mouse support, now we can also use detectMouse() function to set snes_mouse automatically. Example was updated to display this function. MouseSpeedChange funciton was renamed to mouseSpeedChange(). My bad ^^ - Pad folder name was changed to "input" - input folder inside old pad folder was renamed to "controller", as well as other assets - pad.h was renamed to input.h, and every reference to that file inside other files - input name folder was changed to "controller", as well as assets inside - Some minor tabs P.D. Sorry for the big change! I think that input is a more suitable name, now that we have more controlling devices in addition to pad controllers. ***SUPER SCOPE USAGE*** first, we might use detectSuperScope() on boot to detect Super Scope presence. Other way is to force detection by populating snes_sscope to 1 manually, but we dont need to do that if we call this function. We need to call this function everytime Scope gets disconnected from the system, a usefull way to do it is inside this conditional: ``` if (snes_sscope == false) { detectSuperScope(); // some other code you might need in your program, like displaying warning messages and stopping your game. } ``` Here is a brief explanation of every variable we might be using: ``` extern u16 scope_holddelay; /*! \brief Hold delay. */ extern u16 scope_repdelay; /*! \brief Repeat rate. */ extern u16 scope_shothraw; /*! \brief Horizontal shot position, not adjusted. */ extern u16 scope_shotvraw; /*! \brief Vertical shot position, not adjusted. */ extern u16 scope_shoth; /*! \brief Horizontal shot position, adjusted for aim. */ extern u16 scope_shotv; /*! \brief Vertical shot position, adjusted for aim. */ extern u16 scope_centerh; /*! \brief 0x0000 is the center of the screen, positive values go to bottom right. */ extern u16 scope_centerv; /*! \brief 0x0000 is the center of the screen, positive values go to bottom right. */ extern u16 scope_down; /*! \brief flags that are currently true.*/ extern u16 scope_now; /*! \brief flags that have become true this frame.*/ extern u16 scope_held; /*! \brief flagsthat have been true for a certain length of time.*/ extern u16 scope_last; /*! \brief flags that were true on the previous frame.*/ extern u16 scope_sinceshot; /*! \brief Number of frames elapsed since last shot was fired.*/ ``` for scope_down, scope_now, scope_held, scope_last, we need to mask our bits with this usefull bits: ``` typedef enum SUPERSCOPE_BITS { SSC_FIRE = BIT(15), //!< superscope FIRE button. SSC_CURSOR = BIT(14), //!< superscope CURSOR button. SSC_PAUSE = BIT(12), //!< superscope PAUSE button. SSC_TURBO = BIT(13), //!< superscope TURBO flag. SSC_OFFSCREEN = BIT(9), //!< superscope OFFSCREEN flag. SSC_NOISE = BIT(8), //!< superscope NOISE flag. } SUPERSCOPE_BITS; ``` And that's most of it. You can look inside the example file to have an idea of how you can program Super Scope games. ***MOUSE USAGE UPDATE*** Same as Super Scope, mouse uses detectMouse() to populate snes_mouse to 1, so It can be called at boot and like this: ``` if (snes_mouse == false) { detectMouse(); // some other code you might need in your program, like displaying warning messages and stopping your game. } ``` Co-Authored-By: alekmaul --- pvsneslib/include/snes.h | 11 +- pvsneslib/include/snes/console.h | 2 +- pvsneslib/include/snes/{pad.h => input.h} | 90 ++- pvsneslib/source/consoles.asm | 4 + pvsneslib/source/{pads.asm => input.asm} | 427 +++++++----- pvsneslib/source/libc.asm | 2 +- snes-examples/{pads => input}/Makefile | 0 .../{pads/input => input/controller}/Makefile | 2 +- .../input.c => input/controller/controller.c} | 2 +- .../{pads/input => input/controller}/data.asm | 0 .../{pads/input => input/controller}/hdr.asm | 0 .../controller}/pvsneslibfont.bmp | Bin snes-examples/{pads => input}/mouse/Makefile | 0 .../{pads => input}/mouse/buttons.png | Bin .../{pads => input}/mouse/cursor.png | Bin snes-examples/{pads => input}/mouse/data.asm | 0 snes-examples/{pads => input}/mouse/hdr.asm | 0 snes-examples/{pads => input}/mouse/mouse.c | 20 +- .../{pads => input}/mouse/pvsneslibfont.bmp | Bin .../{pads => input}/multiplay5/Makefile | 0 .../{pads => input}/multiplay5/data.asm | 0 .../{pads => input}/multiplay5/hdr.asm | 0 .../{pads => input}/multiplay5/multiplay5.c | 0 .../multiplay5/pvsneslibfont.bmp | Bin snes-examples/input/superscope/Makefile | 31 + snes-examples/input/superscope/SuperScope.c | 610 ++++++++++++++++++ snes-examples/input/superscope/SuperScope.h | 53 ++ .../input/superscope/calibration_target.png | Bin 0 -> 7940 bytes snes-examples/input/superscope/data.asm | 30 + snes-examples/input/superscope/hdr.asm | 46 ++ .../input/superscope/pvsneslibfont.bmp | Bin 0 -> 7222 bytes snes-examples/input/superscope/sin_cos.h | 145 +++++ snes-examples/input/superscope/sprites.png | Bin 0 -> 8516 bytes 33 files changed, 1270 insertions(+), 205 deletions(-) rename pvsneslib/include/snes/{pad.h => input.h} (61%) rename pvsneslib/source/{pads.asm => input.asm} (75%) rename snes-examples/{pads => input}/Makefile (100%) rename snes-examples/{pads/input => input/controller}/Makefile (96%) rename snes-examples/{pads/input/input.c => input/controller/controller.c} (98%) rename snes-examples/{pads/input => input/controller}/data.asm (100%) rename snes-examples/{pads/input => input/controller}/hdr.asm (100%) rename snes-examples/{pads/input => input/controller}/pvsneslibfont.bmp (100%) rename snes-examples/{pads => input}/mouse/Makefile (100%) rename snes-examples/{pads => input}/mouse/buttons.png (100%) rename snes-examples/{pads => input}/mouse/cursor.png (100%) rename snes-examples/{pads => input}/mouse/data.asm (100%) rename snes-examples/{pads => input}/mouse/hdr.asm (100%) rename snes-examples/{pads => input}/mouse/mouse.c (96%) rename snes-examples/{pads => input}/mouse/pvsneslibfont.bmp (100%) rename snes-examples/{pads => input}/multiplay5/Makefile (100%) rename snes-examples/{pads => input}/multiplay5/data.asm (100%) rename snes-examples/{pads => input}/multiplay5/hdr.asm (100%) rename snes-examples/{pads => input}/multiplay5/multiplay5.c (100%) rename snes-examples/{pads => input}/multiplay5/pvsneslibfont.bmp (100%) create mode 100644 snes-examples/input/superscope/Makefile create mode 100644 snes-examples/input/superscope/SuperScope.c create mode 100644 snes-examples/input/superscope/SuperScope.h create mode 100644 snes-examples/input/superscope/calibration_target.png create mode 100644 snes-examples/input/superscope/data.asm create mode 100644 snes-examples/input/superscope/hdr.asm create mode 100644 snes-examples/input/superscope/pvsneslibfont.bmp create mode 100644 snes-examples/input/superscope/sin_cos.h create mode 100644 snes-examples/input/superscope/sprites.png diff --git a/pvsneslib/include/snes.h b/pvsneslib/include/snes.h index cc29dbb3..eb57c616 100644 --- a/pvsneslib/include/snes.h +++ b/pvsneslib/include/snes.h @@ -52,7 +52,7 @@ - \ref interrupt.h "Interrupts" \section user_io_api User Input/output - - \ref pad.h "Keypad" + - \ref input.h "Keypad" - \ref console.h "Console and Debug Printing" \section engine_api Engine API functions @@ -121,9 +121,10 @@ \example graphics/Palette/GetColors/GetColors.c - \example pads/input/input.c - \example pads/mouse/mouse.c - \example pads/multiplay5/multiplay5.c + \example input/controller/controller.c + \example input/mouse/mouse.c + \example input/multiplay5/multiplay5.c + \example input/superscope/superscope.c \example timer/timer.c @@ -177,10 +178,10 @@ #include "snes/background.h" #include "snes/console.h" #include "snes/dma.h" +#include "snes/input.h" #include "snes/interrupt.h" #include "snes/map.h" #include "snes/object.h" -#include "snes/pad.h" #include "snes/scores.h" #include "snes/sound.h" #include "snes/sprite.h" diff --git a/pvsneslib/include/snes/console.h b/pvsneslib/include/snes/console.h index 14621864..69e74ecb 100644 --- a/pvsneslib/include/snes/console.h +++ b/pvsneslib/include/snes/console.h @@ -36,8 +36,8 @@ #include #include +#include #include -#include #include #include #include diff --git a/pvsneslib/include/snes/pad.h b/pvsneslib/include/snes/input.h similarity index 61% rename from pvsneslib/include/snes/pad.h rename to pvsneslib/include/snes/input.h index 7b53495f..d4c8278f 100644 --- a/pvsneslib/include/snes/pad.h +++ b/pvsneslib/include/snes/input.h @@ -1,6 +1,6 @@ /*--------------------------------------------------------------------------------- - Pads registers + Input registers Copyright (C) 2012-2013 Alekmaul @@ -27,12 +27,12 @@ ---------------------------------------------------------------------------------*/ -/*! \file pad.h - \brief pad support. +/*! \file input.h + \brief input support. */ -#ifndef SNES_PADS_INCLUDE -#define SNES_PADS_INCLUDE +#ifndef SNES_INPUT_INCLUDE +#define SNES_INPUT_INCLUDE #include #include @@ -59,22 +59,51 @@ typedef enum KEYPAD_BITS KEY_Y = BIT(14), //!< pad Y button. } KEYPAD_BITS; +/*! \file + \brief common values for SuperScope input. +*/ +//! enum values for the SuperScope buttons and flags. +typedef enum SUPERSCOPE_BITS +{ + SSC_FIRE = BIT(15), //!< superscope FIRE button. + SSC_CURSOR = BIT(14), //!< superscope CURSOR button. + SSC_PAUSE = BIT(12), //!< superscope PAUSE button. + SSC_TURBO = BIT(13), //!< superscope TURBO flag. + SSC_OFFSCREEN = BIT(9), //!< superscope OFFSCREEN flag. + SSC_NOISE = BIT(8), //!< superscope NOISE flag. +} SUPERSCOPE_BITS; + extern u16 pad_keys[2]; extern u16 pad_keysold[2]; extern u16 pad_keysrepeat[2]; -extern u8 snes_mplay5; /*!< \brief 1 if MultiPlay5 connected */ -extern u8 snes_mouse; /*!< \brief 1 if Mouse is going to be used */ +extern u8 snes_mplay5; /*! \brief 1 if MultiPlay5 is connected */ +extern u8 snes_mouse; /*! \brief 1 if Mouse is going to be used */ +extern u8 snes_sscope; /*! \brief 1 if SuperScope is connected */ extern u8 mouseConnect[2]; /*! \brief 1 if Mouse present */ extern u8 mouseButton[2]; /*! \brief 1 if button is pressed, stays for a bit and then it gets released (Click mode). */ extern u8 mousePressed[2]; /*! \brief 1 if button is pressed, stays until is unpressed (Turbo mode). */ extern u8 mouse_x[2], mouse_y[2]; /*! \brief Mouse acceleration. daaaaaaa, d = direction (0: up/left, 1: down/right), a = acceleration. */ -extern u8 mouseSpeedSet[2]; /*! \brief Mouse speed setting. 0: slow, 1: normal, 2: fast */ +extern u8 mouseSpeedSet[2]; /*! \brief Mouse speed setting. 0: slow, 1: normal, 2: fast */ #define mouse_L 0x01 /*! \brief SNES Mouse Left button mask.*/ #define mouse_R 0x02 /*! \brief SNES Mouse Right button mask.*/ +extern u16 scope_holddelay; /*! \brief Hold delay. */ +extern u16 scope_repdelay; /*! \brief Repeat rate. */ +extern u16 scope_shothraw; /*! \brief Horizontal shot position, not adjusted. */ +extern u16 scope_shotvraw; /*! \brief Vertical shot position, not adjusted. */ +extern u16 scope_shoth; /*! \brief Horizontal shot position, adjusted for aim. */ +extern u16 scope_shotv; /*! \brief Vertical shot position, adjusted for aim. */ +extern u16 scope_centerh; /*! \brief 0x0000 is the center of the screen, positive values go to bottom right. */ +extern u16 scope_centerv; /*! \brief 0x0000 is the center of the screen, positive values go to bottom right. */ +extern u16 scope_down; /*! \brief flags that are currently true.*/ +extern u16 scope_now; /*! \brief flags that have become true this frame.*/ +extern u16 scope_held; /*! \brief flagsthat have been true for a certain length of time.*/ +extern u16 scope_last; /*! \brief flags that were true on the previous frame.*/ +extern u16 scope_sinceshot; /*! \brief Number of frames elapsed since last shot was fired.*/ + /*! \def REG_JOYxLH \brief SNES Controllers I/O Ports - Automatic Reading. @@ -161,15 +190,54 @@ void detectMPlay5(void); */ void scanMPlay5(void); +/*! \fn detectMouse(void) + \brief Check if Mouse is connected and populate snes_mouse (0 or 1 for connected) +*/ +void detectMouse(void); + /*! \fn mouseRead(void) \brief Wait for mouse ready and read mouse values in. */ void mouseRead(void); -/*! \fn MouseSpeedChange(u8 port) +/*! \fn mouseSpeedChange(u8 port) \brief Set mouse hardware speed (populate mouseSpeed[] first). \param port Specify wich port to use (0-1) */ -void MouseSpeedChange(u8 port); +void mouseSpeedChange(u8 port); + +/*! \fn detectSuperScope(void) + \brief Detects if SuperScope is connected on Port 1 (second controller port on console) and populate snes_sscope (0 or 1 for connected) +*/ +void detectSuperScope(void); + +/*! \fn scanScope(void) + \brief Nintendo SHVC Scope BIOS version 1.00 + Quickly disassembled and commented by Revenant on 31 Jan 2013 + + This assembly uses xkas v14 syntax. It probably also assembles with bass, if there's + any such thing as good fortune in the universe. + + How to use the SHVC Super Scope BIOS: + (all variables are two bytes) + + 1: Set "HoldDelay" and "RepDelay" for the button hold delay and repeat rate + + 2: "jsr GetScope" or "jsl GetScopeLong" once per frame + + 3: Read one of the following to get the scope input bits (see definitions below): + - ScopeDown (for any flags that are currently true) + - ScopeNow (for any flags that have become true this frame) + - ScopeHeld (for any flags that have been true for a certain length of time) + - ScopeLast (for any flags that were true on the previous frame) + + 3a: If the bits read from ScopeNow indicate a valid shot, or if the Cursor button + is being pressed, then read "ShotH"/"ShotV" to adjust for aim, or read + "ShotHRaw"/"ShotVRaw" for "pure" coordinates + + 3c: at some point, set "CenterH"/"CenterV" equal to "ShotHRaw"/"ShotVRaw" + so that the aim-adjusted coordinates are "correct" +*/ +void scanScope(void); -#endif // SNES_PADS_INCLUDE +#endif // SNES_PADS_INCLUDE \ No newline at end of file diff --git a/pvsneslib/source/consoles.asm b/pvsneslib/source/consoles.asm index f62229dd..02921789 100644 --- a/pvsneslib/source/consoles.asm +++ b/pvsneslib/source/consoles.asm @@ -390,6 +390,9 @@ consoleVblank: and mouseConnect + 1 ; If both ports have a mouse plugged, it will skip pad controller reading bne cvbloam + jsl scanPads + lda snes_sscope + beq cvbloam + jsl scanScope cvbloam: ; Put oam to screen if needed @@ -450,6 +453,7 @@ consoleInit: sta scr_txt_dirty ; Nothing to print on screen sta snes_mplay5 ; For Pad function sta snes_mouse ; Set mouse usage disabled by default + sta snes_sscope ; Set superscope usage disabled by default phb pha diff --git a/pvsneslib/source/pads.asm b/pvsneslib/source/input.asm similarity index 75% rename from pvsneslib/source/pads.asm rename to pvsneslib/source/input.asm index bce18ccd..9cd8af4c 100644 --- a/pvsneslib/source/pads.asm +++ b/pvsneslib/source/input.asm @@ -26,21 +26,24 @@ .equ REG_JOYA $4016 .equ REG_JOYB $4017 .equ REG_WRIO $4201 -.equ REG_HVBJOY $4212 +.equ REG_HVBJOY $4212 .equ REG_JOY1L $4218 .equ REG_JOY2L $421A +.equ REG_OPHCT $213C ; Horizontal scanline location + .BASE $00 .RAMSECTION ".reg_pads" BANK 0 SLOT 1 -pad_keys dsb 10 ; 5 pads , 16 bits reg -pad_keysold dsb 10 ; 5 pads , 16 bits reg -pad_keysrepeat dsb 10 ; 5 pads , 16 bits reg +pad_keys dsb 10 ; 5 pads , 16 bits reg +pad_keysold dsb 10 ; 5 pads , 16 bits reg +pad_keysrepeat dsb 10 ; 5 pads , 16 bits reg -snes_mplay5 db ; 1 if MultiPlayer5 connected -mp5read db ; for multiplayer5 plug test +snes_mplay5 db ; 1 if MultiPlayer5 is connected +mp5read db ; for multiplayer5 plug test +snes_sscope db ; 1 if SuperScope is connected ; Port 2 input pressed, held, and pressed last frame (2 bytes each) ; These are for the BIOS use only. Use the Super Scope input vars below instead. @@ -62,7 +65,7 @@ scope_tohold dsb 2 ; 0x0200 - offscreen ; 0x0100 - noise scope_down dsb 2 -scope_now dsb 2 +scope_now dsb 2 scope_held dsb 2 scope_last dsb 2 @@ -184,15 +187,15 @@ padsClear: pha plb - rep #$20 - lda 8,s ; get value - pha - plx + rep #$20 + lda 8,s ; get value + pha + plx - sep #$20 - lda #$0 - sta pad_keys,x - sta pad_keysold,x + sep #$20 + lda #$0 + sta pad_keys,x + sta pad_keysold,x sta pad_keysrepeat,x plx @@ -216,20 +219,20 @@ padsDown: pha plb - rep #$20 - lda 8,s ; get value - pha - plx + rep #$20 + lda 8,s ; get value + pha + plx - lda pad_keysold,x - eor #$FFFF - sta.w tcc__r0 + lda pad_keysold,x + eor #$FFFF + sta.w tcc__r0 - lda pad_keys,x - and.w tcc__r0 - sta.w tcc__r0 + lda pad_keys,x + and.w tcc__r0 + sta.w tcc__r0 - plx + plx plb plp rtl @@ -251,21 +254,21 @@ padsUp: pha plb - rep #$20 - lda 8,s ; get value - pha - plx + rep #$20 + lda 8,s ; get value + pha + plx - lda pad_keys,x - eor #$FFFF - sta.w tcc__r0 + lda pad_keys,x + eor #$FFFF + sta.w tcc__r0 - lda pad_keys,x - eor.w pad_keysold,x - and.w tcc__r0 - sta.w tcc__r0 + lda pad_keys,x + eor.w pad_keysold,x + and.w tcc__r0 + sta.w tcc__r0 - plx + plx plb plp rtl @@ -318,10 +321,10 @@ checkmplay5ston: checkmplay5stoff: lda REG_JOYB ; read 8 times bit1 and store values - lsr ; bit1->0 + lsr ; bit1->0 and #$1 ; only this bit ora mp5read ; add current value and store it - dex + dex beq + asl sta mp5read @@ -374,7 +377,7 @@ scanMPlay5: bcs - sep #$20 - lda.b #$80 ; enable iobit to read data + lda.b #$80 ; enable iobit to read data sta.w REG_WRIO lda.b #$1 @@ -384,11 +387,11 @@ scanMPlay5: rep #$20 ldy #16 getpad1data: ; get all 16 bits pad1 data serialy - lda.w REG_JOYA - lsr a ; put bit0 into carry - rol.w pad_keys ; pad 1 data - dey - bne getpad1data + lda.w REG_JOYA + lsr a ; put bit0 into carry + rol.w pad_keys ; pad 1 data + dey + bne getpad1data ldy #16 getpad23data: ; get all 16 bits pad2&3 data serialy @@ -407,12 +410,12 @@ getpad23data: ; get all 16 bits pad2&3 data serialy ldy #16 getpad45data: ; get all 16 bits pad2&3 data serialy lda.w REG_JOYB - lsr a ; put bit1 into carry - rol.w pad_keys+6 ; pad 4 data - lsr a ; put bit1 into carry - rol.w pad_keys+8 ; pad 5 data - dey - bne getpad45data + lsr a ; put bit1 into carry + rol.w pad_keys+6 ; pad 4 data + lsr a ; put bit1 into carry + rol.w pad_keys+8 ; pad 5 data + dey + bne getpad45data lda pad_keys eor pad_keysold ; compute 'down' state from bits that @@ -436,7 +439,7 @@ getpad45data: ; get all 16 bits pad2&3 data serialy sta pad_keysrepeat+8 sep #$20 - lda.b #$80 ; enable iobit for next frame + lda.b #$80 ; enable iobit for next frame sta.w REG_WRIO ply @@ -587,7 +590,7 @@ GetButtons: lda scope_port2down sta scope_held - lda scope_repdelay ; set the remaining delay to the repeat value + lda scope_repdelay ; set the remaining delay to the repeat value sta scope_tohold bra NotHeld @@ -608,10 +611,41 @@ NoScope: stz scope_down stz scope_now stz scope_held + stz snes_sscope ; and lib flag plp ; return from input check rts +;--------------------------------------------------------------------------------- +; detectSuperScope(void) +detectSuperScope: + php + phb + + sep #$20 + lda #$0 ; change bank address to 0 + pha + plb + +-: lda REG_HVBJOY + and.b #$01 + bne - + + rep #$20 + lda REG_JOY2L ; Get joypad 2 input and check if the controller in port 2 is a Super Scope. + and.w #$0CFF ; For a 16-bit auto joypad read, bits 0-7 should be always 1 + cmp.w #$00FF ; and bits 10-11 should be always 0. + bne + + + sep #$20 + lda #$01 + sta snes_sscope + ++: + plb + plp + rtl + .ENDS ;--------------------------------------------------------------------------------- @@ -644,183 +678,224 @@ NoScope: ;--------------------------------------------------------------------------------- ; void mouseRead(void) mouseRead: - php - sep #$30 - phb - phx - phy + php + sep #$30 + phb + phx + phy - lda #$00 ; Set Data Bank to 0 - pha - plb + lda #$00 ; Set Data Bank to 0 + pha + plb _10: - lda REG_HVBJOY - and #$01 - bne _10 ; Automatic read ok? + lda REG_HVBJOY + and #$01 + bne _10 ; Automatic read ok? - ldx #$01 - lda REG_JOY2L ; Joy2 - jsr mouse_data + ldx #$01 + lda REG_JOY2L ; Joy2 + jsr mouse_data - lda connect_st+1 - beq _20 + lda connect_st+1 + beq _20 - jsr speed_change - stz connect_st+1 + jsr speed_change + stz connect_st+1 - bra _30 + bra _30 _20: - dex - lda REG_JOY1L ; Joy1 + dex + lda REG_JOY1L ; Joy1 - jsr mouse_data + jsr mouse_data - lda connect_st - beq _30 + lda connect_st + beq _30 - jsr speed_change - stz connect_st + jsr speed_change + stz connect_st _30: - ply - plx - plb - plp - rtl + ply + plx + plb + plp + rtl mouse_data: - sta tcc__r0 ; (421A / 4218 saved to reg0) - and.b #$0F - cmp.b #$01 ; Is the mouse connected? - beq _m10 + sta tcc__r0 ; (421A / 4218 saved to reg0) + and.b #$0F + cmp.b #$01 ; Is the mouse connected? + beq _m10 + + stz mouseConnect,x ; No connection. - stz mouseConnect,x ; No connection. + stz mouseButton,x + stz mousePressed,x + stz mouse_x,x + stz mouse_y,x - stz mouseButton,x - stz mousePressed,x - stz mouse_x,x - stz mouse_y,x + stz snes_mouse + + rts - rts _m10: - lda mouseConnect,x ; When mouse is connected, speed will change. - bne _m20 ; Previous connection status + lda mouseConnect,x ; When mouse is connected, speed will change. + bne _m20 ; Previous connection status ; (mouse.com judged by lower 1 bit) - lda #$01 ; Connection check flag on - sta mouseConnect,x - sta connect_st,x - rts + lda #$01 ; Connection check flag on + sta mouseConnect,x + sta connect_st,x + rts + _m20: - rep #$10 - ldy #16 ; Read 16 bit data. - sep #$10 + rep #$10 + ldy #16 ; Read 16 bit data. + sep #$10 + _m30: - lda REG_JOYA,x + lda REG_JOYA,x - lsr a - rol mouse_x,x - rol mouse_y,x - dey - bne _m30 + lsr a + rol mouse_x,x + rol mouse_y,x + dey + bne _m30 - stz mousePressed,x + stz mousePressed,x - rol tcc__r0 - rol mousePressed,x - rol tcc__r0 - rol mousePressed,x ; Switch turbo + rol tcc__r0 + rol mousePressed,x + rol tcc__r0 + rol mousePressed,x ; Switch turbo - lda mousePressed,x - eor mouse_sb,x ; Get switch trigger - bne _m40 + lda mousePressed,x + eor mouse_sb,x ; Get switch trigger + bne _m40 - stz mouseButton,x + stz mouseButton,x + + rts - rts _m40: - lda mousePressed,x - sta mouseButton,x - sta mouse_sb,x + lda mousePressed,x + sta mouseButton,x + sta mouse_sb,x - rts + rts ;--------------------------------------------------------------------------------- -; void MouseSpeedChange(u8 port) -MouseSpeedChange: - php - sep #$30 - phb - phx - phy +; void mouseSpeedChange(u8 port) +mouseSpeedChange: + php + sep #$30 + phb + phx + phy - lda #$00 ; Set Data Bank to 0 - pha - plb + lda #$00 ; Set Data Bank to 0 + pha + plb - lda 8,s ; Set port - tax + lda 8,s ; Set port + tax - jsr speed_change + jsr speed_change - ply - plx - plb - plp - rtl + ply + plx + plb + plp + rtl speed_change: - php - sep #$30 + php + sep #$30 + + lda mouseConnect,x + beq _s25 - lda mouseConnect,x - beq _s25 + lda #$10 + sta tcc__r0h - lda #$10 - sta tcc__r0h _s10: - lda #$01 - sta REG_JOYA - lda REG_JOYA,x ; Speed change (1 step) - stz REG_JOYA + lda #$01 + sta REG_JOYA + lda REG_JOYA,x ; Speed change (1 step) + stz REG_JOYA + + lda #$01 ; Read speed data. + sta REG_JOYA ; Shift register clear. + lda #$00 + sta REG_JOYA - lda #$01 ; Read speed data. - sta REG_JOYA ; Shift register clear. - lda #$00 - sta REG_JOYA + sta mouse_sp,x ; Speed register clear. - sta mouse_sp,x ; Speed register clear. + ldy #10 ; Shift register read has no meaning - ldy #10 ; Shift register read has no meaning _s20: - lda REG_JOYA,x - dey - bne _s20 + lda REG_JOYA,x + dey + bne _s20 + + lda REG_JOYA,x ; Read speed - lda REG_JOYA,x ; Read speed + lsr a + rol mouse_sp,x - lsr a - rol mouse_sp,x + lda REG_JOYA, x - lda REG_JOYA, x + lsr a + rol mouse_sp,x + lda mouse_sp,x - lsr a - rol mouse_sp,x - lda mouse_sp,x + cmp mouseSpeedSet,x ; Set speed or not? - cmp mouseSpeedSet,x ; Set speed or not? + beq _s30 - beq _s30 + dec tcc__r0h ; For error check + bne _s10 - dec tcc__r0h ; For error check - bne _s10 _s25: - lda #$80 ; Speed change error. - sta mouse_sp,x + lda #$80 ; Speed change error. + sta mouse_sp,x + _s30: - plp - rts + plp + rts + +;--------------------------------------------------------------------------------- +; detectMouse(void) +detectMouse: + php + phb + + sep #$20 + lda #$0 ; change bank address to 0 + pha + plb + +-: lda REG_HVBJOY + and.b #$01 + bne - + + rep #$20 + lda REG_JOY1L + ora REG_JOY2L + and.w #$000F + cmp.w #$0001 ; Is the mouse connected on any port? + bne + + + sep #$20 + lda #$01 + sta snes_mouse + ++: + plb + plp + rtl .ENDS \ No newline at end of file diff --git a/pvsneslib/source/libc.asm b/pvsneslib/source/libc.asm index ea8613d1..aa982d3e 100644 --- a/pvsneslib/source/libc.asm +++ b/pvsneslib/source/libc.asm @@ -473,11 +473,11 @@ exitl4: .include "backgrounds.asm" .include "consoles.asm" .include "dmas.asm" +.include "input.asm" .include "interrupts.asm" .include "lzsss.asm" .include "maps.asm" .include "objects.asm" -.include "pads.asm" .include "scores.asm" .include "snesmodwla.asm" .include "sounds.asm" diff --git a/snes-examples/pads/Makefile b/snes-examples/input/Makefile similarity index 100% rename from snes-examples/pads/Makefile rename to snes-examples/input/Makefile diff --git a/snes-examples/pads/input/Makefile b/snes-examples/input/controller/Makefile similarity index 96% rename from snes-examples/pads/input/Makefile rename to snes-examples/input/controller/Makefile index 8e174d4b..090aaee7 100644 --- a/snes-examples/pads/input/Makefile +++ b/snes-examples/input/controller/Makefile @@ -8,7 +8,7 @@ include ${PVSNESLIB_HOME}/devkitsnes/snes_rules #--------------------------------------------------------------------------------- # ROMNAME is used in snes_rules file -export ROMNAME := input +export ROMNAME := controller all: bitmaps $(ROMNAME).sfc diff --git a/snes-examples/pads/input/input.c b/snes-examples/input/controller/controller.c similarity index 98% rename from snes-examples/pads/input/input.c rename to snes-examples/input/controller/controller.c index 960ca646..3068c353 100644 --- a/snes-examples/pads/input/input.c +++ b/snes-examples/input/controller/controller.c @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------- - snes pad demo + snes pad controller demo -- alekmaul diff --git a/snes-examples/pads/input/data.asm b/snes-examples/input/controller/data.asm similarity index 100% rename from snes-examples/pads/input/data.asm rename to snes-examples/input/controller/data.asm diff --git a/snes-examples/pads/input/hdr.asm b/snes-examples/input/controller/hdr.asm similarity index 100% rename from snes-examples/pads/input/hdr.asm rename to snes-examples/input/controller/hdr.asm diff --git a/snes-examples/pads/input/pvsneslibfont.bmp b/snes-examples/input/controller/pvsneslibfont.bmp similarity index 100% rename from snes-examples/pads/input/pvsneslibfont.bmp rename to snes-examples/input/controller/pvsneslibfont.bmp diff --git a/snes-examples/pads/mouse/Makefile b/snes-examples/input/mouse/Makefile similarity index 100% rename from snes-examples/pads/mouse/Makefile rename to snes-examples/input/mouse/Makefile diff --git a/snes-examples/pads/mouse/buttons.png b/snes-examples/input/mouse/buttons.png similarity index 100% rename from snes-examples/pads/mouse/buttons.png rename to snes-examples/input/mouse/buttons.png diff --git a/snes-examples/pads/mouse/cursor.png b/snes-examples/input/mouse/cursor.png similarity index 100% rename from snes-examples/pads/mouse/cursor.png rename to snes-examples/input/mouse/cursor.png diff --git a/snes-examples/pads/mouse/data.asm b/snes-examples/input/mouse/data.asm similarity index 100% rename from snes-examples/pads/mouse/data.asm rename to snes-examples/input/mouse/data.asm diff --git a/snes-examples/pads/mouse/hdr.asm b/snes-examples/input/mouse/hdr.asm similarity index 100% rename from snes-examples/pads/mouse/hdr.asm rename to snes-examples/input/mouse/hdr.asm diff --git a/snes-examples/pads/mouse/mouse.c b/snes-examples/input/mouse/mouse.c similarity index 96% rename from snes-examples/pads/mouse/mouse.c rename to snes-examples/input/mouse/mouse.c index 6cda3c65..d812b121 100644 --- a/snes-examples/pads/mouse/mouse.c +++ b/snes-examples/input/mouse/mouse.c @@ -43,12 +43,12 @@ int main(void) // Initialize SNES consoleInit(); - snes_mouse = true; // Let's tell the system we're using mouse bios (after init) - // we set mouse speed, or it will just output a random speed. We can change it later manually mouseSpeedSet[0] = slow; mouseSpeedSet[1] = slow; + detectMouse(); // Let's check if a mouse is plugged in any port on boot + // Init cursors sprite oamInitGfxSet(&cursorsprite, (&cursorsprite_end - &cursorsprite), &cursorpal, 48 * 2, 0, 0x0000, OBJ_SIZE16_L32); @@ -103,6 +103,9 @@ int main(void) while (1) { + if (snes_mouse == false) + detectMouse(); // Let's check if a mouse is plugged in any port + odd++; // Optimize Draw text by printing new text just once if (mouseConnect[0] != mc_mem[0]) @@ -305,19 +308,19 @@ int main(void) { mouseSpeedSet[0] = slow; speedset[0] = true; - MouseSpeedChange(0); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. + mouseSpeedChange(0); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. } if ((p1_mouse_x > 0x6C) && (p1_mouse_x < 0x94)) { mouseSpeedSet[0] = normal; speedset[0] = true; - MouseSpeedChange(0); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. + mouseSpeedChange(0); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. } if ((p1_mouse_x > 0x9C) && (p1_mouse_x < 0xBC)) { mouseSpeedSet[0] = fast; speedset[0] = true; - MouseSpeedChange(0); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. + mouseSpeedChange(0); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. } } } @@ -366,20 +369,19 @@ int main(void) { mouseSpeedSet[1] = slow; speedset[1] = true; - MouseSpeedChange(1); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. + mouseSpeedChange(1); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. } if ((p2_mouse_x > 0x6C) && (p2_mouse_x < 0x94)) { mouseSpeedSet[1] = normal; speedset[1] = true; - MouseSpeedChange(1); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. + mouseSpeedChange(1); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. } if ((p2_mouse_x > 0x9C) && (p2_mouse_x < 0xBC)) { mouseSpeedSet[1] = fast; speedset[1] = true; - consoleMesenBreakpoint(); - MouseSpeedChange(1); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. + mouseSpeedChange(1); // Let's tell the mouse we want to change speed. mouseSpeedSet[] has to be populated first. } } } diff --git a/snes-examples/pads/mouse/pvsneslibfont.bmp b/snes-examples/input/mouse/pvsneslibfont.bmp similarity index 100% rename from snes-examples/pads/mouse/pvsneslibfont.bmp rename to snes-examples/input/mouse/pvsneslibfont.bmp diff --git a/snes-examples/pads/multiplay5/Makefile b/snes-examples/input/multiplay5/Makefile similarity index 100% rename from snes-examples/pads/multiplay5/Makefile rename to snes-examples/input/multiplay5/Makefile diff --git a/snes-examples/pads/multiplay5/data.asm b/snes-examples/input/multiplay5/data.asm similarity index 100% rename from snes-examples/pads/multiplay5/data.asm rename to snes-examples/input/multiplay5/data.asm diff --git a/snes-examples/pads/multiplay5/hdr.asm b/snes-examples/input/multiplay5/hdr.asm similarity index 100% rename from snes-examples/pads/multiplay5/hdr.asm rename to snes-examples/input/multiplay5/hdr.asm diff --git a/snes-examples/pads/multiplay5/multiplay5.c b/snes-examples/input/multiplay5/multiplay5.c similarity index 100% rename from snes-examples/pads/multiplay5/multiplay5.c rename to snes-examples/input/multiplay5/multiplay5.c diff --git a/snes-examples/pads/multiplay5/pvsneslibfont.bmp b/snes-examples/input/multiplay5/pvsneslibfont.bmp similarity index 100% rename from snes-examples/pads/multiplay5/pvsneslibfont.bmp rename to snes-examples/input/multiplay5/pvsneslibfont.bmp diff --git a/snes-examples/input/superscope/Makefile b/snes-examples/input/superscope/Makefile new file mode 100644 index 00000000..45d427eb --- /dev/null +++ b/snes-examples/input/superscope/Makefile @@ -0,0 +1,31 @@ +ifeq ($(strip $(PVSNESLIB_HOME)),) +$(error "Please create an environment variable PVSNESLIB_HOME with path to its folder and restart application. (you can do it on windows with )") +endif + +include ${PVSNESLIB_HOME}/devkitsnes/snes_rules + +.PHONY: bitmaps all + +#--------------------------------------------------------------------------------- +# ROMNAME is used in snes_rules file +export ROMNAME := superscope + +all: bitmaps $(ROMNAME).sfc + +clean: cleanBuildRes cleanRom cleanGfx + rm -f *.m16 *.b16 *.o16 + +#--------------------------------------------------------------------------------- +pvsneslibfont.pic: pvsneslibfont.bmp + @echo convert font with no tile reduction ... $(notdir $@) + $(GFXCONV) -s 8 -o 16 -u 16 -p -e 1 -t bmp -i $< + +sprites.pic: sprites.png + @echo convert sprites bitmap ... $(notdir $@) + $(GFXCONV) -s 32 -u 16 -o 48 -t png -i $< + +calibration_target.pic: calibration_target.png + @echo convert BG map ... $(notdir $@) + $(GFXCONV) -s 8 -u 16 -o 16 -t png -m -i $< + +bitmaps : pvsneslibfont.pic sprites.pic calibration_target.pic \ No newline at end of file diff --git a/snes-examples/input/superscope/SuperScope.c b/snes-examples/input/superscope/SuperScope.c new file mode 100644 index 00000000..bb40f67c --- /dev/null +++ b/snes-examples/input/superscope/SuperScope.c @@ -0,0 +1,610 @@ +/*--------------------------------------------------------------------------------- + + + super scope demo + -- alekmaul + + demo game made by DigiDwrf + + +---------------------------------------------------------------------------------*/ + +#include +#include "superscope.h" +#include "sin_cos.h" + +extern char tilfont, palfont; + +extern unsigned char sprites_map, sprites_map_end, sprites_palette; // Sprites +extern unsigned char cali_target_tileset, cali_target_tileset_end, cali_target_map, cali_target_palette; // BG + +void hideSprites() +{ + u8 i; + for (i = 0; i < 128; i++) + oamSetVisible((i << 2), OBJ_HIDE); // Hide sprites +} + +void resetGame() +{ + // Let's reset everything + consoleDrawText(12, 8, " "); + consoleDrawText(11, 10, " "); + consoleDrawText(3, 14, " "); + consoleDrawText(3, 15, " "); + + hideSprites(); + + targets_shot = 0; + enable_fire = false; + bullet_id = 0; + shot_bullets = 0; + + u8 i; + for (i = 0; i < max_bullets; i++) + { + difuse_xn[i] = 0; + difuse_yn[i] = 0; + difuse_x[i] = 0; + difuse_y[i] = 0; + bullet_frame[i] = 0; + bullet_draw[i] = 0; + bullet_frame_n[i] = 0; + bullet_gravity[i] = 0; + bullet_diff[i] = 0; + } + + for (target_id = 0; target_id < 8; target_id++) + { + target_kill[target_id] = false; + target_kill_count[target_id] = 0; + target_gravity[target_id] = 0; + } +} + +//--------------------------------------------------------------------------------- +// Main program +int main(void) +{ + consoleInit(); // Initialize SNES + + // Initialize text console with our font + consoleSetTextVramBGAdr(0x2000); + consoleSetTextVramAdr(0x3800); + consoleSetTextOffset(0x0080); + consoleInitText(1, 16 * 2, &tilfont, &palfont); + + // Init background + bgInitTileSet(0, &cali_target_tileset, &cali_target_palette, 0, (&cali_target_tileset_end - &cali_target_tileset), 16 * 2, BG_16COLORS, 0x3000); + bgSetGfxPtr(1, 0x3800); + bgSetMapPtr(1, 0x2000, SC_32x32); + + // Init sprites + oamInitGfxSet(&sprites_map, (&sprites_map_end - &sprites_map), &sprites_palette, 48 * 2, 0, 0x0000, OBJ_SIZE16_L32); + + u8 i; + for (i = 0; i < max_targets; i++) + { + oamSet(16 + (i << 2), 0, 0, 2, 0, 0, 0x84, 1); // Target sprites + oamSetEx(16 + (i << 2), OBJ_SMALL, OBJ_HIDE); + } + + // Set mode 1 and disable BG3 + setMode(BG_MODE1, 0); + bgSetDisable(2); + + // Set vertical scrolling to -1, default BG scrolling is one horizontal line above screen. We're doing this to place our center of screen perfectly. + bgSetScroll(0, 0, -1); + bgSetScroll(1, 0, -1); + +START_OVER: + bgInitMapSet(0, &cali_target_map, 0x700, SC_32x32, 0x2800); + + // Draw a wonderful text :P + consoleDrawText(8, 1, "SUPERSCOPE Test"); + + if (pause_adjust) + consoleDrawText(1, 3, "Press PAUSE to return to game"); + else + { + detectSuperScope(); // Let's check if SuperScope is connected on boot + if (snes_sscope == false) + consoleDrawText(1, 26, "Connect SuperScope into Port 2"); + else + consoleDrawText(6, 26, "SuperScope connected!"); + } + + // Sprites setup + oamSet(0, 0, 0, 2, 0, 0, 0x80, 2); // Spot sprite, used for aim adjustment + oamSetEx(0, OBJ_SMALL, OBJ_HIDE); + + setScreenOn(); + + while (snes_sscope == false) // no SuperScope? Let's plug one + { + WaitForVBlank(); + detectSuperScope(); + if (snes_sscope) + consoleDrawText(1, 26, " SuperScope connected! "); + } + + if (pause_adjust == false) + WaitNVBlank(60); // Hold on for one second + +ADJUST_AIM: + consoleDrawText(1, 24, " "); + consoleDrawText(3, 25, " Shoot center of screen "); + consoleDrawText(3, 26, " to adjust aim "); + + while (1) + { + if (snes_sscope == false) + { + detectSuperScope(); + consoleDrawText(5, 25, " "); + consoleDrawText(1, 26, "Connect SuperScope into Port 2"); + sscope_disconnected = true; + } + else + { + if (sscope_disconnected) + { + consoleDrawText(1, 26, " SuperScope connected! "); + WaitNVBlank(60); // Hold on for one second + sscope_disconnected = false; + goto ADJUST_AIM; + } + + if ((scope_down & SSC_FIRE) == false) // Don't go forward until fire is off at start + enable_fire = true; + + if (pause_adjust) + if (scope_down & SSC_PAUSE) // we read pause input from scope to continue game (pause) or adjust aim again (boot) + { + setFadeEffect(FADE_OUT); + oamSet(0, 0, 0, 2, 0, 0, 0, 0); // Restore bullet sprite + oamSetVisible(0, OBJ_HIDE); // Hide sprite + pause_adjust = false; + goto CONTINUE_GAME; + } + + if (enable_fire) + { + if (scope_down & SSC_FIRE) // we read fire input from scope to begin aim adjust, just after VBlank + { + enable_fire = false; + break; + } + } + } + WaitForVBlank(); + } + + // Offsets may vary if your code is different, so you can play with this numbers until you find them suitable for your program. + u8 h_offset = 0x8A; + u8 v_offset = 0x74; + + // Start aim adjust process: it stores scope's raw values relative to the center of screen, so shot matches superscope's physical aim. + scope_centerh = scope_shothraw - h_offset; + scope_centerv = scope_shotvraw - v_offset; + + if (pause_adjust) + consoleDrawText(1, 3, " "); + consoleDrawText(5, 25, " Good. "); + consoleDrawText(8, 26, " "); + + oamSetXY(0, 0x78, 0x68); // Draw spot at center, just for reference, most games do this to tell the player that SuperScope was shot + + WaitNVBlank(60); // Hold on for one second + oamSetVisible(0, OBJ_HIDE); // Hide spot + consoleDrawText(9, 25, "Test your aim"); + + while (1) + { + if (snes_sscope == false) + { + detectSuperScope(); + consoleDrawText(5, 25, " "); + consoleDrawText(1, 26, "Connect SuperScope into Port 2"); + sscope_disconnected = true; + } + else + { + if (sscope_disconnected) + { + consoleDrawText(1, 26, " SuperScope connected! "); + WaitNVBlank(60); // Hold on for one second + sscope_disconnected = false; + goto ADJUST_AIM; + } + + if ((scope_down & SSC_FIRE) == false) // Don't go forward until fire is off at start + enable_fire = true; + + if (enable_fire) + { + if (scope_down & SSC_FIRE) // we read fire input from scope to see if our aim is adjusted + { + enable_fire = false; + break; + } + } + } + WaitForVBlank(); + } + + oamSetXY(0, scope_shoth - 0x12, scope_shotv - 0x0C); // Draw spot in the center if the aim was on the center during aim adjustment, this means our aim is good. + + WaitNVBlank(60); // Hold on for one second + + s8 shot_diff_x = scope_shoth - scope_shothraw; // difference between raw horizontal value and adjusted shot. + s8 shot_diff_y = scope_shotv - scope_shotvraw; // difference between raw vertical value and adjusted shot. + + if ((shot_diff_x + shot_diff_y) == 0) // no difference between adjusted shot coords and raw coords, this means we have a perfect aim adjustment + consoleDrawText(12, 24, "PERFECT!"); + else if ((shot_diff_x > -4) && (shot_diff_x < 4) && (shot_diff_y > -4) && (shot_diff_y < 4)) // Aim in range (8px x 8px) + consoleDrawText(14, 24, "Nice."); // inside 8x8 range, displays a good message + else + consoleDrawText(2, 24, "You can do better. Try again?"); // More than 8x8, we think is too much + if (pause_adjust) + { + consoleDrawText(4, 25, "Press PAUSE to return"); + consoleDrawText(3, 26, "Press CURSOR to adjust aim"); + } + else + { + consoleDrawText(4, 25, "Press PAUSE to adjust aim"); + consoleDrawText(6, 26, "Press CURSOR to play"); + } + + while (1) + { + if (snes_sscope == false) + { + detectSuperScope(); + consoleDrawText(2, 24, " "); + consoleDrawText(4, 25, " "); + consoleDrawText(1, 26, "Connect SuperScope into Port 2"); + oamSetVisible(0, OBJ_HIDE); // Hide spot + sscope_disconnected = true; + } + else + { + if ((scope_down & SSC_CURSOR) == false) // Don't go ahead until cursor is off at start + enable_cursor = true; + + if (sscope_disconnected) + { + consoleDrawText(1, 26, " SuperScope connected! "); + WaitNVBlank(60); // Hold on for one second + sscope_disconnected = false; + goto ADJUST_AIM; + } + + if (scope_down & SSC_PAUSE) // we read pause input from scope to continue game (pause) or adjust aim again (boot) + { + oamSetVisible(0, OBJ_HIDE); // Hide spot + if (pause_adjust) + { + setFadeEffect(FADE_OUT); + oamSet(0, 0, 0, 2, 0, 0, 0, 0); // Restore bullet sprite + oamSetVisible(0, OBJ_HIDE); // Hide sprite + pause_adjust = false; + goto CONTINUE_GAME; + } + else + goto ADJUST_AIM; + } + + if (enable_cursor) + if (scope_down & SSC_CURSOR) // we read cursor input from scope to adjust aim again (pause) or begin game (boot) + { + enable_cursor = false; + if (pause_adjust) + { + oamSetVisible(0, OBJ_HIDE); // Hide spot + consoleDrawText(1, 3, "Press PAUSE to return to game"); + goto ADJUST_AIM; + } + else + break; + } + } + + WaitForVBlank(); + } + + oamSetVisible(0, OBJ_HIDE); // Hide spot + + /* Before we enter main game, let's setup bullet sprites, we need them to be bigger at first, make them dissapear into the horizon and draw new bullets over old ones, + so we're going to set multiple sprites and make them appear one after another, like a "set of cards". No coordinates are needed at this time. */ + + while (bullet_frame[0] < 8) + { + oamSet(bullet_draw[0] << 2, 0, 0, 2, 0, 0, bullet_frames[bullet_frame[0]], 0); + oamSetVisible(bullet_draw[0] << 2, OBJ_HIDE); // Hide sprite + bullet_draw[0]++; + if (bullet_draw[0] == 4) // We skip some sprite IDs, so targets are in between bullets, for a more realistic behavior + bullet_draw[0] = 68; + + bullet_frame_n[0]++; + if (bullet_frame_n[0] > ((bullet_frame[0] - 4) << 3)) // This will just repeat multiple bullet sprites into OAM, so they seem to go far away + { + bullet_frame[0]++; + bullet_frame_n[0] = 0; + } + } + + // We also have to setup Fire rate + scope_holddelay = max_rate; // How much time we need to consider a button has been held + scope_repdelay = max_rate; // Fire rate + + setFadeEffect(FADE_OUT); + +PLAY_GAME: + + // Initial positions of targets + for (target_id = 0; target_id < 8; target_id++) + { + target_x_inc[target_id] = target_id << 6; + target_y_inc[target_id] = target_id << 6; + target_oam_id[target_id] = 16 + target_id << 2; + } + + bool print_once = true; // We'll use this later to print text just once inside our loop + u8 text_timer = 0; + +CONTINUE_GAME: + + if (text_timer < 120) + { + consoleDrawText(13, 13, "SHOOT"); + consoleDrawText(10, 14, "THE TARGETS!"); + } + + consoleDrawText(8, 1, " "); + consoleDrawText(1, 3, " "); + consoleDrawText(8, 4, " "); + consoleDrawText(1, 24, " "); + consoleDrawText(4, 25, " "); + consoleDrawText(3, 26, " "); + + setPaletteColor(0, 0x7FFF); // Let's use a white BACK + setPaletteColor(17, 0x0000); // and black for texts + + WaitForVBlank(); + dmaFillVram(&cali_target_map, 0x2800, 0x700); // Wipe screen + oamInitGfxAttr(0x0000, OBJ_SIZE32_L64); // Set to bigger sprites + + setScreenOn(); + + // MAIN GAME + while (1) + { + switch (text_timer) + { + case 120: // Wait two seconds before we wipe text from screen + consoleDrawText(13, 13, " "); + consoleDrawText(10, 14, " "); + default: + text_timer++; + break; + case 121: + // Do nothing + break; + } + + if (snes_sscope == false) + { + detectSuperScope(); + consoleDrawText(1, 13, "Connect SuperScope into Port 2"); + consoleDrawText(3, 14, " "); + consoleDrawText(3, 15, " "); + sscope_disconnected = true; + } + else + { + if (sscope_disconnected) + { + consoleDrawText(1, 13, " SuperScope connected! "); + WaitNVBlank(60); // Hold on for one second + consoleDrawText(6, 13, " "); + sscope_disconnected = false; + } + + if ((scope_down & SSC_FIRE) == false) // Don't shoot until fire is off at start + enable_fire = true; + if ((scope_down & SSC_PAUSE) == false) // Don't pause until pause is off at start + enable_pause = true; + if ((scope_down & SSC_CURSOR) == false) // Don't play again until cursor is off at start + enable_cursor = true; + + if (enable_fire) + { + if (scope_held & SSC_FIRE) + { + bullet_id++; + if (bullet_id == 32) + bullet_id = 0; + + bullet_x[bullet_id] = scope_shoth - 0x1A; + bullet_y[bullet_id] = scope_shotv - 0x14; + bullet_frame[bullet_id] = 0; + bullet_draw[bullet_id] = 0; + + // To make more of a natural shot, we add some random directions and gravity + bullet_gravity[bullet_id] = 0; + difuse_xn[bullet_id] = 0; + difuse_yn[bullet_id] = 0; + difuse_x[bullet_id] = (rand() & 0x0F) - 8; + difuse_y[bullet_id] = ((rand() & 0xF0) >> 4) - 8; + + shot_bullets++; // incease shot bullets by one + } + + for (bullet_num = 0; bullet_num < shot_bullets; bullet_num++) + { + s8 id = bullet_id - bullet_num; + if (id < 0) + id += 32; // To avoid negative overflow on reset + + if (bullet_frame[id] < 8) + { + if (bullet_draw[id]) + oamSetVisible((bullet_draw[id] - bullet_diff[id]) << 2, OBJ_HIDE); // Remove used sprites + difuse_xn[id] += difuse_x[id]; + bullet_gravity[id]++; + difuse_yn[id] += difuse_y[id] + (bullet_gravity[id] >> 2); + + oamSetXY(bullet_draw[id] << 2, bullet_x[id] + (difuse_xn[id] >> 6), bullet_y[id] + (difuse_yn[id] >> 6)); // Draw new bullets sprites on screen + + bullet_draw[id]++; + if (bullet_draw[id] == 4) + { + bullet_draw[id] = 68; // We skip some sprite IDs, so targets are in between bullets, for a more realistic behavior + bullet_diff[id] = 65; + } + else + bullet_diff[id] = 1; + + bullet_frame_n[id]++; + if (bullet_frame_n[id] > ((bullet_frame[id] - 4) << 3)) + { + bullet_frame[id]++; + bullet_frame_n[id] = 0; + } + } + else if (bullet_frame[id] == 8) + { + oamSetVisible((bullet_draw[id] - 1) << 2, OBJ_HIDE); // Remove last used sprite + shot_bullets--; // decrease shot bullets by one + bullet_frame[id]++; // just to be sure this part of code executes just once + } + } + } + + // DRAW TARGETS ON SCREEN + + if (targets_shot < 8) + { + // Pause routine + if (enable_pause) + if (scope_down & SSC_PAUSE) + { + setFadeEffect(FADE_OUT); + consoleDrawText(13, 13, " "); + consoleDrawText(10, 14, " "); + + setPalette(&cali_target_palette, 0, 2); // Restore blue BACK + setPaletteColor(17, 0x7FFF); // Restore white text + + hideSprites(); + pause_adjust = true; + + enable_fire = false; + enable_cursor = false; + goto START_OVER; + } + + // We are going to create a rotating carrousel made of 8 targets + for (target_id = 0; target_id < 8; target_id++) + { + if (target_kill[target_id] == false) + { + target_x_inc[target_id]++; + target_y_inc[target_id]++; + if (target_x_inc[target_id] > 511) + target_x_inc[target_id] = 0; + if (target_y_inc[target_id] > 511) + target_y_inc[target_id] = 0; + + target_x[target_id] = ((cos_table(target_x_inc[target_id]) << 1) / 3) + 27; + target_y[target_id] = ((sin_table(target_y_inc[target_id]) << 1) / 3) + 11; + + oamSetXY(target_oam_id[target_id], target_x[target_id], target_y[target_id]); // Target sprites + + target_collision_x[target_id] = oamGetX(target_oam_id[target_id]) - oamGetX(12); // id 3 (* 4) is the fourth frame of bullet animation. We use this one to collide with target + target_collision_y[target_id] = oamGetY(target_oam_id[target_id]) - oamGetY(12); // id 3 (* 4) is the fourth frame of bullet animation. We use this one to collide with target + + if ((target_collision_x[target_id] > -12) && (target_collision_x[target_id] < 12) && (target_collision_y[target_id] > -12) && (target_collision_y[target_id] < 12)) + target_kill[target_id] = true; + } + + // Kill target if it was "touched" by a bullet + if (target_kill[target_id]) + { + switch (target_kill_count[target_id]) + { + case 0: + oamSetGfxOffset(target_oam_id[target_id], 0x88); + target_x[target_id] -= 16; // Fix larger object offset + target_y[target_id] -= 16; // Fix larger object offset + oamSetXY(target_oam_id[target_id], target_x[target_id], target_y[target_id]); + oamSetEx(target_oam_id[target_id], OBJ_LARGE, OBJ_SHOW); + break; + case 3: + oamSetGfxOffset(target_oam_id[target_id], 0x100); + break; + case 6: + oamSetGfxOffset(target_oam_id[target_id], 0x108); + break; + case 12: + oamSetGfxOffset(target_oam_id[target_id], 0x84); // Restore target sprite GFX + oamSetEx(target_oam_id[target_id], OBJ_SMALL, OBJ_HIDE); // Restore small size and hide sprite + targets_shot++; + enable_pause = false; + enable_cursor = false; + target_kill_count[target_id]++; // Get out of case + break; + } + if (target_kill_count[target_id] < 12) + { + target_gravity[target_id]++; + oamSetXY(target_oam_id[target_id], target_x[target_id], target_y[target_id] + (target_gravity[target_id] >> 2)); + target_kill_count[target_id]++; + + if (target_kill_count[target_id] & 0x02) + oamSetVisible(target_oam_id[target_id], OBJ_HIDE); // Blink + } + } + } + } + else // We have won! + { + if (print_once == true) + { + consoleDrawText(12, 8, "YOU WIN!"); + consoleDrawText(11, 10, "Play again?"); + print_once = false; + } + + consoleDrawText(3, 14, "Press CURSOR to play again"); + consoleDrawText(3, 15, "Press PAUSE to adjust aim"); + + if (enable_cursor) + if (scope_down & SSC_CURSOR) + { + setFadeEffect(FADE_OUT); + + resetGame(); + goto PLAY_GAME; + } + + if (enable_pause) + if (scope_down & SSC_PAUSE) + { + setFadeEffect(FADE_OUT); + + resetGame(); + setPalette(&cali_target_palette, 0, 2); // Restore blue BACK + setPaletteColor(17, 0x7FFF); // Restore white text + goto START_OVER; + } + } + } + + WaitForVBlank(); + } + + return 0; +} \ No newline at end of file diff --git a/snes-examples/input/superscope/SuperScope.h b/snes-examples/input/superscope/SuperScope.h new file mode 100644 index 00000000..9fd63b5e --- /dev/null +++ b/snes-examples/input/superscope/SuperScope.h @@ -0,0 +1,53 @@ +#ifndef SUPERSCOPE_H +#define SUPERSCOPE_H + +#define max_rate 10 // To put some limit to turbo mode, so we're gonna fire every 10 frames + +#define max_bullets 32 +#define max_targets 64 + +bool sscope_disconnected = false; // if SuperScope is disconnected during gameplay, and plugged again, we can control some behavior +bool pause_adjust = false; // If we pause during game, will lead us to calibration screen, but we will be able to continue our game + +// Let's declare some usefull variables to animate our fantastic blue bullets :D +u8 bullet_id = 0; // bullet id, we're going to use a maximum of 32 bullets, and then they reset to 0 +u8 bullet_num = 0; // bullet number, this will help us to draw multiple bullets on screen at the same time +u8 shot_bullets = 0; // the number of bullets we have shot and need to be drawn on screen. Every time a bullet dissapears, this number will decrease by one. + +u16 bullet_x[max_bullets]; +u8 bullet_y[max_bullets]; + +s16 difuse_xn[max_bullets] = {0}; +s16 difuse_yn[max_bullets] = {0}; +s8 difuse_x[max_bullets] = {0}; +s8 difuse_y[max_bullets] = {0}; +u8 bullet_frame[max_bullets] = {0}; +u8 bullet_draw[max_bullets] = {0}; +u8 bullet_frame_n[max_bullets] = {0}; +u8 bullet_gravity[max_bullets] = {0}; +u8 bullet_diff[max_bullets] = {1}; + +u8 bullet_frames[] = {0, 4, 8, 12, 64, 68, 72, 76}; // GFX positions for the OAM table + +bool enable_fire = false; +bool enable_pause = false; +bool enable_cursor = false; + +// And out targets +u8 target_id = 0; +u8 target_oam_id[max_targets]; + +u16 target_x_inc[max_targets]; +u16 target_y_inc[max_targets]; + +u16 target_x[max_targets]; +u8 target_y[max_targets]; +s8 target_collision_x[max_targets]; +s8 target_collision_y[max_targets]; + +bool target_kill[max_targets] = {false}; +u8 target_kill_count[max_targets] = {0}; +u8 target_gravity[max_targets] = {0}; +u8 targets_shot = 0; + +#endif \ No newline at end of file diff --git a/snes-examples/input/superscope/calibration_target.png b/snes-examples/input/superscope/calibration_target.png new file mode 100644 index 0000000000000000000000000000000000000000..aadbb8ef5c602e1ed579cf7a20baff16c4da09c4 GIT binary patch literal 7940 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 literal 0 HcmV?d00001 diff --git a/snes-examples/input/superscope/sin_cos.h b/snes-examples/input/superscope/sin_cos.h new file mode 100644 index 00000000..77501999 --- /dev/null +++ b/snes-examples/input/superscope/sin_cos.h @@ -0,0 +1,145 @@ +#ifndef SINCOS_H +#define SINCOS_H + +const u8 precalculated_sin_table[] = { + 0x7F, 0x81, 0x82, 0x84, 0x86, 0x87, 0x89, 0x8A, + 0x8C, 0x8D, 0x8F, 0x90, 0x92, 0x94, 0x95, 0x97, + 0x98, 0x9A, 0x9B, 0x9D, 0x9E, 0xA0, 0xA1, 0xA3, + 0xA4, 0xA6, 0xA7, 0xA9, 0xAA, 0xAC, 0xAD, 0xAF, + 0xB0, 0xB1, 0xB3, 0xB4, 0xB6, 0xB7, 0xB9, 0xBA, + 0xBB, 0xBD, 0xBE, 0xBF, 0xC1, 0xC2, 0xC3, 0xC5, + 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCC, 0xCE, 0xCF, + 0xD0, 0xD1, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, + 0xD9, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, + 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE7, 0xE8, + 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xED, 0xEE, 0xEF, + 0xF0, 0xF0, 0xF1, 0xF2, 0xF3, 0xF3, 0xF4, 0xF4, + 0xF5, 0xF6, 0xF6, 0xF7, 0xF7, 0xF8, 0xF8, 0xF9, + 0xF9, 0xFA, 0xFA, 0xFB, 0xFB, 0xFB, 0xFC, 0xFC, + 0xFC, 0xFD, 0xFD, 0xFD, 0xFD, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFD, 0xFD, 0xFD, 0xFD, + 0xFC, 0xFC, 0xFC, 0xFB, 0xFB, 0xFB, 0xFA, 0xFA, + 0xF9, 0xF9, 0xF8, 0xF8, 0xF7, 0xF7, 0xF6, 0xF6, + 0xF5, 0xF4, 0xF4, 0xF3, 0xF3, 0xF2, 0xF1, 0xF0, + 0xF0, 0xEF, 0xEE, 0xED, 0xED, 0xEC, 0xEB, 0xEA, + 0xE9, 0xE8, 0xE7, 0xE7, 0xE6, 0xE5, 0xE4, 0xE3, + 0xE2, 0xE1, 0xE0, 0xDF, 0xDE, 0xDD, 0xDC, 0xDB, + 0xD9, 0xD8, 0xD7, 0xD6, 0xD5, 0xD4, 0xD3, 0xD1, + 0xD0, 0xCF, 0xCE, 0xCC, 0xCB, 0xCA, 0xC9, 0xC7, + 0xC6, 0xC5, 0xC3, 0xC2, 0xC1, 0xBF, 0xBE, 0xBD, + 0xBB, 0xBA, 0xB9, 0xB7, 0xB6, 0xB4, 0xB3, 0xB1, + 0xB0, 0xAF, 0xAD, 0xAC, 0xAA, 0xA9, 0xA7, 0xA6, + 0xA4, 0xA3, 0xA1, 0xA0, 0x9E, 0x9D, 0x9B, 0x9A, + 0x98, 0x97, 0x95, 0x94, 0x92, 0x90, 0x8F, 0x8D, + 0x8C, 0x8A, 0x89, 0x87, 0x86, 0x84, 0x82, 0x81, + 0x7F, 0x7E, 0x7C, 0x7B, 0x79, 0x77, 0x76, 0x74, + 0x73, 0x71, 0x70, 0x6E, 0x6D, 0x6B, 0x69, 0x68, + 0x66, 0x65, 0x63, 0x62, 0x60, 0x5F, 0x5D, 0x5C, + 0x5A, 0x59, 0x57, 0x56, 0x54, 0x53, 0x51, 0x50, + 0x4E, 0x4D, 0x4C, 0x4A, 0x49, 0x47, 0x46, 0x45, + 0x43, 0x42, 0x40, 0x3F, 0x3E, 0x3C, 0x3B, 0x3A, + 0x38, 0x37, 0x36, 0x35, 0x33, 0x32, 0x31, 0x30, + 0x2E, 0x2D, 0x2C, 0x2B, 0x2A, 0x28, 0x27, 0x26, + 0x25, 0x24, 0x23, 0x22, 0x21, 0x20, 0x1F, 0x1E, + 0x1D, 0x1C, 0x1B, 0x1A, 0x19, 0x18, 0x17, 0x16, + 0x15, 0x14, 0x14, 0x13, 0x12, 0x11, 0x10, 0x10, + 0x0F, 0x0E, 0x0D, 0x0D, 0x0C, 0x0B, 0x0B, 0x0A, + 0x09, 0x09, 0x08, 0x08, 0x07, 0x07, 0x06, 0x06, + 0x05, 0x05, 0x04, 0x04, 0x04, 0x03, 0x03, 0x03, + 0x02, 0x02, 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, + 0x02, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x05, + 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08, 0x09, + 0x09, 0x0A, 0x0B, 0x0B, 0x0C, 0x0D, 0x0D, 0x0E, + 0x0F, 0x10, 0x10, 0x11, 0x12, 0x13, 0x14, 0x14, + 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, + 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, + 0x25, 0x26, 0x27, 0x28, 0x2A, 0x2B, 0x2C, 0x2D, + 0x2E, 0x30, 0x31, 0x32, 0x33, 0x35, 0x36, 0x37, + 0x38, 0x3A, 0x3B, 0x3C, 0x3E, 0x3F, 0x40, 0x42, + 0x43, 0x45, 0x46, 0x47, 0x49, 0x4A, 0x4C, 0x4D, + 0x4E, 0x50, 0x51, 0x53, 0x54, 0x56, 0x57, 0x59, + 0x5A, 0x5C, 0x5D, 0x5F, 0x60, 0x62, 0x63, 0x65, + 0x66, 0x68, 0x69, 0x6B, 0x6D, 0x6E, 0x70, 0x71, + 0x73, 0x74, 0x76, 0x77, 0x79, 0x7B, 0x7C, 0x7E}; + +const u8 precalculated_cos_table[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFD, 0xFD, 0xFD, 0xFD, + 0xFC, 0xFC, 0xFC, 0xFB, 0xFB, 0xFB, 0xFA, 0xFA, + 0xF9, 0xF9, 0xF8, 0xF8, 0xF7, 0xF7, 0xF6, 0xF6, + 0xF5, 0xF4, 0xF4, 0xF3, 0xF3, 0xF2, 0xF1, 0xF0, + 0xF0, 0xEF, 0xEE, 0xED, 0xED, 0xEC, 0xEB, 0xEA, + 0xE9, 0xE8, 0xE7, 0xE7, 0xE6, 0xE5, 0xE4, 0xE3, + 0xE2, 0xE1, 0xE0, 0xDF, 0xDE, 0xDD, 0xDC, 0xDB, + 0xD9, 0xD8, 0xD7, 0xD6, 0xD5, 0xD4, 0xD3, 0xD1, + 0xD0, 0xCF, 0xCE, 0xCC, 0xCB, 0xCA, 0xC9, 0xC7, + 0xC6, 0xC5, 0xC3, 0xC2, 0xC1, 0xBF, 0xBE, 0xBD, + 0xBB, 0xBA, 0xB9, 0xB7, 0xB6, 0xB4, 0xB3, 0xB1, + 0xB0, 0xAF, 0xAD, 0xAC, 0xAA, 0xA9, 0xA7, 0xA6, + 0xA4, 0xA3, 0xA1, 0xA0, 0x9E, 0x9D, 0x9B, 0x9A, + 0x98, 0x97, 0x95, 0x94, 0x92, 0x90, 0x8F, 0x8D, + 0x8C, 0x8A, 0x89, 0x87, 0x86, 0x84, 0x82, 0x81, + 0x7F, 0x7E, 0x7C, 0x7B, 0x79, 0x77, 0x76, 0x74, + 0x73, 0x71, 0x70, 0x6E, 0x6D, 0x6B, 0x69, 0x68, + 0x66, 0x65, 0x63, 0x62, 0x60, 0x5F, 0x5D, 0x5C, + 0x5A, 0x59, 0x57, 0x56, 0x54, 0x53, 0x51, 0x50, + 0x4E, 0x4D, 0x4C, 0x4A, 0x49, 0x47, 0x46, 0x45, + 0x43, 0x42, 0x40, 0x3F, 0x3E, 0x3C, 0x3B, 0x3A, + 0x38, 0x37, 0x36, 0x35, 0x33, 0x32, 0x31, 0x30, + 0x2E, 0x2D, 0x2C, 0x2B, 0x2A, 0x28, 0x27, 0x26, + 0x25, 0x24, 0x23, 0x22, 0x21, 0x20, 0x1F, 0x1E, + 0x1D, 0x1C, 0x1B, 0x1A, 0x19, 0x18, 0x17, 0x16, + 0x15, 0x14, 0x14, 0x13, 0x12, 0x11, 0x10, 0x10, + 0x0F, 0x0E, 0x0D, 0x0D, 0x0C, 0x0B, 0x0B, 0x0A, + 0x09, 0x09, 0x08, 0x08, 0x07, 0x07, 0x06, 0x06, + 0x05, 0x05, 0x04, 0x04, 0x04, 0x03, 0x03, 0x03, + 0x02, 0x02, 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, + 0x02, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x05, + 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08, 0x09, + 0x09, 0x0A, 0x0B, 0x0B, 0x0C, 0x0D, 0x0D, 0x0E, + 0x0F, 0x10, 0x10, 0x11, 0x12, 0x13, 0x14, 0x14, + 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, + 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, + 0x25, 0x26, 0x27, 0x28, 0x2A, 0x2B, 0x2C, 0x2D, + 0x2E, 0x30, 0x31, 0x32, 0x33, 0x35, 0x36, 0x37, + 0x38, 0x3A, 0x3B, 0x3C, 0x3E, 0x3F, 0x40, 0x42, + 0x43, 0x45, 0x46, 0x47, 0x49, 0x4A, 0x4C, 0x4D, + 0x4E, 0x50, 0x51, 0x53, 0x54, 0x56, 0x57, 0x59, + 0x5A, 0x5C, 0x5D, 0x5F, 0x60, 0x62, 0x63, 0x65, + 0x66, 0x68, 0x69, 0x6B, 0x6D, 0x6E, 0x70, 0x71, + 0x73, 0x74, 0x76, 0x77, 0x79, 0x7B, 0x7C, 0x7E, + 0x7F, 0x81, 0x82, 0x84, 0x86, 0x87, 0x89, 0x8A, + 0x8C, 0x8D, 0x8F, 0x90, 0x92, 0x94, 0x95, 0x97, + 0x98, 0x9A, 0x9B, 0x9D, 0x9E, 0xA0, 0xA1, 0xA3, + 0xA4, 0xA6, 0xA7, 0xA9, 0xAA, 0xAC, 0xAD, 0xAF, + 0xB0, 0xB1, 0xB3, 0xB4, 0xB6, 0xB7, 0xB9, 0xBA, + 0xBB, 0xBD, 0xBE, 0xBF, 0xC1, 0xC2, 0xC3, 0xC5, + 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCC, 0xCE, 0xCF, + 0xD0, 0xD1, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, + 0xD9, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, + 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE7, 0xE8, + 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xED, 0xEE, 0xEF, + 0xF0, 0xF0, 0xF1, 0xF2, 0xF3, 0xF3, 0xF4, 0xF4, + 0xF5, 0xF6, 0xF6, 0xF7, 0xF7, 0xF8, 0xF8, 0xF9, + 0xF9, 0xFA, 0xFA, 0xFB, 0xFB, 0xFB, 0xFC, 0xFC, + 0xFC, 0xFD, 0xFD, 0xFD, 0xFD, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + +u8 sin_table(u16 number) +{ + return precalculated_sin_table[number]; +} +u8 cos_table(u16 number) +{ + return precalculated_cos_table[number]; +} + +#endif \ No newline at end of file diff --git a/snes-examples/input/superscope/sprites.png b/snes-examples/input/superscope/sprites.png new file mode 100644 index 0000000000000000000000000000000000000000..82c09f090afb9a77444943651c07d1d7851cc61c GIT binary patch literal 8516 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 AV*mgE literal 0 HcmV?d00001 From 7c0026468f07ea9bebdcb8d646ee832767fb98c0 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Tue, 9 Apr 2024 05:35:38 +0200 Subject: [PATCH 041/106] chore(*): add chapter for mouse and superscope --- wiki/Input-and-Output.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/wiki/Input-and-Output.md b/wiki/Input-and-Output.md index 09d1bdf7..bf9e4286 100644 --- a/wiki/Input-and-Output.md +++ b/wiki/Input-and-Output.md @@ -118,7 +118,10 @@ pvsneslibfont.pic: pvsneslibfont.bmp ## Input -Currently, the only input managed with _PVSneslib_ is the **SNES joypad**. + _PVSneslib_ is able to manage the **SNES joypad**, the **mouse" and the "**SuperScope**. + +### Joypad + ``` __--L--_________________--R--__ Button Colors: / _ \ PAL and Japan North America @@ -172,3 +175,13 @@ At least, the pad is refresh during VBL (thanks to VBlank function), so it is no consoleDrawText(12,10,"A PRESSED"); } ``` + +### Mouse + +** WORK IN PROGRESS ** + + +### SuperScope + +** WORK IN PROGRESS ** + From 00279bba4bbc4a8fb591cee89a93b86ce269517b Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Tue, 9 Apr 2024 05:56:41 +0200 Subject: [PATCH 042/106] chore(*): typo fixes --- pvsneslib/include/snes.h | 4 +- pvsneslib/source/input.asm | 4 +- readme.md | 3 +- wiki/Input-and-Output.md | 86 +++++++++++++++++++++++++++++++++++++- 4 files changed, 90 insertions(+), 7 deletions(-) diff --git a/pvsneslib/include/snes.h b/pvsneslib/include/snes.h index eb57c616..573716ef 100644 --- a/pvsneslib/include/snes.h +++ b/pvsneslib/include/snes.h @@ -75,7 +75,7 @@ - Neoflash Menu google code. - Devkitpro team for pvsneslib structure (lib, makefile, examples, and so on ...). - undisbeliever for his great platform code example on github. - - digidwrf for fastrom / hirom support. + - digidwrf for fastrom / hirom support, mouse and superscope support. */ // adding the example page. @@ -120,7 +120,7 @@ \example graphics/Palette/GetColors/GetColors.c - + \example input/controller/controller.c \example input/mouse/mouse.c \example input/multiplay5/multiplay5.c diff --git a/pvsneslib/source/input.asm b/pvsneslib/source/input.asm index 9cd8af4c..07d07f7f 100644 --- a/pvsneslib/source/input.asm +++ b/pvsneslib/source/input.asm @@ -1,7 +1,7 @@ ;--------------------------------------------------------------------------------- ; -; Copyright (C) 2013-2020 -; Alekmaul +; Copyright (C) 2013-2024 +; Alekmaul - DigiDwrf ; ; This software is provided 'as-is', without any express or implied ; warranty. In no event will the authors be held liable for any diff --git a/readme.md b/readme.md index 7f77095c..5e80f248 100644 --- a/readme.md +++ b/readme.md @@ -99,7 +99,8 @@ Sydney Hunter by [CollectorVision](https://collectorvision.com/store/shop/snes/s - **RedBug** for constify tcc bug fix and tips for Linux and Docker. - [**Mills32**](https://github.com/mills32/) for his mode7 3D example. - [**N_Arno**](https://github.com/nArnoSNES/) for his help on Linux version. -- [**DigiDwrf**](https://github.com/DigiDwrf/) for hirom / fastrom support. +- [**DigiDwrf**](https://github.com/DigiDwrf/) for hirom / fastrom support and also mouse & superscope support. +- [**undisbeliever**](https://github.com/undisbeliever/castle_platformer/) for map engine example. And, of course, all the [**discord community**](https://discord.gg/DzEFnhB) ! diff --git a/wiki/Input-and-Output.md b/wiki/Input-and-Output.md index bf9e4286..e3c77504 100644 --- a/wiki/Input-and-Output.md +++ b/wiki/Input-and-Output.md @@ -178,10 +178,92 @@ At least, the pad is refresh during VBL (thanks to VBlank function), so it is no ### Mouse -** WORK IN PROGRESS ** +**snes_mouse** has to be turned on (snes_mouse = 1). This is set 0 by default after consoleInit(). +This will tell the system to read from a mouse, if it is found. +Mouse and pads can be used simultaneously, on any ports. Externs in the pad.h file goes like this: + +``` +snes_mouse; /*!1 if Mouse is going to be used */ +``` + +mouseConnect[2]; /* 1 if Mouse present */ +mouseButton[2]; /* 1 if button is pressed, stays for a bit and then it gets released (Click mode). */ +mousePressed[2]; /* 1 if button is pressed, stays until is unpressed (Turbo mode). */ +mouse_x[2], mouse_y[2]; /* Mouse acceleration. daaaaaaa, d = direction (0: up/left, 1: down/right), a = acceleration. */ +mouseSpeedSet[2]; /* Mouse speed setting. 0: slow, 1: normal, 2: fast */ + +First, we might use **detectMouse()** to populate snes_mouse to 1, so It can be called at boot and like this: + +``` +if (snes_mouse == false) +{ + detectMouse(); + // some other code you might need in your program, like displaying warning messages and stopping your game. +} +``` + +the number inside array specifies port (0 or 1). I recommend using this code structure to convert raw acceleration into usable values: + +``` +u16 p1_mouse_x = 0x80; +u16 p1_mouse_y = 0x70; +u16 p2_mouse_x = 0x80; +u16 p2_mouse_y = 0x70; + + if (mouse_x[0] & 0x80) + p1_mouse_x -= mouse_x[0] & 0x7F; +else + p1_mouse_x += mouse_x[0] & 0x7F; +if (mouse_y[0] & 0x80) + p1_mouse_y -= mouse_y[0] & 0x7F; +else + p1_mouse_y += mouse_y[0] & 0x7F; +``` + +And that's most of it. You can look inside the example file **(input** folder) to have an idea of how you can program Mouse games. ### SuperScope -** WORK IN PROGRESS ** +First, we might use **detectSuperScope()** on boot to detect Super Scope presence. Other way is to force detection by populating snes_sscope to 1 manually, but we dont need to do that if we call this function. We need to call this function everytime Scope gets disconnected from the system, a usefull way to do it is inside this conditional: + +``` +if (snes_sscope == false) +{ + detectSuperScope(); + // some other code you might need in your program, like displaying warning messages and stopping your game. +} +``` + +Here is a brief explanation of every variable we might be using: + +``` +extern u16 scope_holddelay; /*! \brief Hold delay. */ +extern u16 scope_repdelay; /*! \brief Repeat rate. */ +extern u16 scope_shothraw; /*! \brief Horizontal shot position, not adjusted. */ +extern u16 scope_shotvraw; /*! \brief Vertical shot position, not adjusted. */ +extern u16 scope_shoth; /*! \brief Horizontal shot position, adjusted for aim. */ +extern u16 scope_shotv; /*! \brief Vertical shot position, adjusted for aim. */ +extern u16 scope_centerh; /*! \brief 0x0000 is the center of the screen, positive values go to bottom right. */ +extern u16 scope_centerv; /*! \brief 0x0000 is the center of the screen, positive values go to bottom right. */ +extern u16 scope_down; /*! \brief flags that are currently true.*/ +extern u16 scope_now; /*! \brief flags that have become true this frame.*/ +extern u16 scope_held; /*! \brief flagsthat have been true for a certain length of time.*/ +extern u16 scope_last; /*! \brief flags that were true on the previous frame.*/ +extern u16 scope_sinceshot; /*! \brief Number of frames elapsed since last shot was fired.*/ +for scope_down, scope_now, scope_held, scope_last, we need to mask our bits with this usefull bits: + +typedef enum SUPERSCOPE_BITS +{ + SSC_FIRE = BIT(15), //!< superscope FIRE button. + SSC_CURSOR = BIT(14), //!< superscope CURSOR button. + SSC_PAUSE = BIT(12), //!< superscope PAUSE button. + SSC_TURBO = BIT(13), //!< superscope TURBO flag. + SSC_OFFSCREEN = BIT(9), //!< superscope OFFSCREEN flag. + SSC_NOISE = BIT(8), //!< superscope NOISE flag. +} SUPERSCOPE_BITS; +``` + +And that's most of it. You can look inside the example file **(input** folder) to have an idea of how you can program Super Scope games. + From 4199b78431649650a5b49b7a8b4da870686259b3 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Tue, 9 Apr 2024 05:59:28 +0200 Subject: [PATCH 043/106] chore(*): typo fixes --- wiki/Input-and-Output.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wiki/Input-and-Output.md b/wiki/Input-and-Output.md index e3c77504..20129b59 100644 --- a/wiki/Input-and-Output.md +++ b/wiki/Input-and-Output.md @@ -118,9 +118,9 @@ pvsneslibfont.pic: pvsneslibfont.bmp ## Input - _PVSneslib_ is able to manage the **SNES joypad**, the **mouse" and the "**SuperScope**. + _PVSneslib_ is able to manage the **SNES joypad**, the **mouse** and the **SuperScope**. -### Joypad +### Joypad ``` __--L--_________________--R--__ Button Colors: @@ -220,7 +220,7 @@ else p1_mouse_y += mouse_y[0] & 0x7F; ``` -And that's most of it. You can look inside the example file **(input** folder) to have an idea of how you can program Mouse games. +And that's most of it. You can look inside the example file (**snes-examples/input** folder) to have an idea of how you can program Mouse games. ### SuperScope @@ -264,6 +264,6 @@ typedef enum SUPERSCOPE_BITS } SUPERSCOPE_BITS; ``` -And that's most of it. You can look inside the example file **(input** folder) to have an idea of how you can program Super Scope games. +And that's most of it. You can look inside the example file (**snes-examples/input** folder) to have an idea of how you can program Super Scope games. From 47e194d18b1cd167802931030d648901cd7fa858 Mon Sep 17 00:00:00 2001 From: Carlos O'Connor Date: Tue, 9 Apr 2024 04:34:42 -0500 Subject: [PATCH 044/106] Super Scope Example update Calibration now works as it should. scope_held functions are best explained too. Offscreen sprites now are hidden. Removed some unnecessary messages because we don't know if the fist aim adjust shot is in the middle of screen, so it's just a test screen. Noise and Offscreen bits are not used in this example, but they are present on lib. --- snes-examples/input/superscope/Makefile | 4 +- snes-examples/input/superscope/SuperScope.c | 80 +++++++++++------- snes-examples/input/superscope/SuperScope.h | 5 +- ...ation_target.png => aim_adjust_target.png} | Bin snes-examples/input/superscope/data.asm | 14 +-- snes-examples/input/superscope/hdr.asm | 2 +- 6 files changed, 63 insertions(+), 42 deletions(-) rename snes-examples/input/superscope/{calibration_target.png => aim_adjust_target.png} (100%) diff --git a/snes-examples/input/superscope/Makefile b/snes-examples/input/superscope/Makefile index 45d427eb..ed4b524b 100644 --- a/snes-examples/input/superscope/Makefile +++ b/snes-examples/input/superscope/Makefile @@ -24,8 +24,8 @@ sprites.pic: sprites.png @echo convert sprites bitmap ... $(notdir $@) $(GFXCONV) -s 32 -u 16 -o 48 -t png -i $< -calibration_target.pic: calibration_target.png +aim_adjust_target.pic: aim_adjust_target.png @echo convert BG map ... $(notdir $@) $(GFXCONV) -s 8 -u 16 -o 16 -t png -m -i $< -bitmaps : pvsneslibfont.pic sprites.pic calibration_target.pic \ No newline at end of file +bitmaps : pvsneslibfont.pic sprites.pic aim_adjust_target.pic \ No newline at end of file diff --git a/snes-examples/input/superscope/SuperScope.c b/snes-examples/input/superscope/SuperScope.c index bb40f67c..c76b4b99 100644 --- a/snes-examples/input/superscope/SuperScope.c +++ b/snes-examples/input/superscope/SuperScope.c @@ -16,7 +16,7 @@ extern char tilfont, palfont; extern unsigned char sprites_map, sprites_map_end, sprites_palette; // Sprites -extern unsigned char cali_target_tileset, cali_target_tileset_end, cali_target_map, cali_target_palette; // BG +extern unsigned char aim_target_tileset, aim_target_tileset_end, aim_target_map, aim_target_palette; // BG void hideSprites() { @@ -75,7 +75,7 @@ int main(void) consoleInitText(1, 16 * 2, &tilfont, &palfont); // Init background - bgInitTileSet(0, &cali_target_tileset, &cali_target_palette, 0, (&cali_target_tileset_end - &cali_target_tileset), 16 * 2, BG_16COLORS, 0x3000); + bgInitTileSet(0, &aim_target_tileset, &aim_target_palette, 0, (&aim_target_tileset_end - &aim_target_tileset), 16 * 2, BG_16COLORS, 0x3000); bgSetGfxPtr(1, 0x3800); bgSetMapPtr(1, 0x2000, SC_32x32); @@ -98,7 +98,7 @@ int main(void) bgSetScroll(1, 0, -1); START_OVER: - bgInitMapSet(0, &cali_target_map, 0x700, SC_32x32, 0x2800); + bgInitMapSet(0, &aim_target_map, 0x700, SC_32x32, 0x2800); // Draw a wonderful text :P consoleDrawText(8, 1, "SUPERSCOPE Test"); @@ -132,7 +132,7 @@ int main(void) WaitNVBlank(60); // Hold on for one second ADJUST_AIM: - consoleDrawText(1, 24, " "); + consoleDrawText(9, 24, " "); consoleDrawText(3, 25, " Shoot center of screen "); consoleDrawText(3, 26, " to adjust aim "); @@ -180,13 +180,10 @@ int main(void) WaitForVBlank(); } - // Offsets may vary if your code is different, so you can play with this numbers until you find them suitable for your program. - u8 h_offset = 0x8A; - u8 v_offset = 0x74; - - // Start aim adjust process: it stores scope's raw values relative to the center of screen, so shot matches superscope's physical aim. - scope_centerh = scope_shothraw - h_offset; - scope_centerv = scope_shotvraw - v_offset; + // Start aim adjust process: it stores scope's raw values relative to the center of screen, so shot matches superscope's physical aim + // We also move our scope's center to the center of screen + scope_centerh = 0x80 - scope_shothraw; + scope_centerv = 0x70 - scope_shotvraw; if (pause_adjust) consoleDrawText(1, 3, " "); @@ -233,22 +230,15 @@ int main(void) WaitForVBlank(); } - oamSetXY(0, scope_shoth - 0x12, scope_shotv - 0x0C); // Draw spot in the center if the aim was on the center during aim adjustment, this means our aim is good. + // To finish aim adjust we store coords relative to the center of screen + oamSetXY(0, scope_shoth - 0x08, scope_shotv - 0x08); // Draw spot aligned with our aim if the aim was on the center on first shot, this means our aim is adjusted. WaitNVBlank(60); // Hold on for one second - s8 shot_diff_x = scope_shoth - scope_shothraw; // difference between raw horizontal value and adjusted shot. - s8 shot_diff_y = scope_shotv - scope_shotvraw; // difference between raw vertical value and adjusted shot. - - if ((shot_diff_x + shot_diff_y) == 0) // no difference between adjusted shot coords and raw coords, this means we have a perfect aim adjustment - consoleDrawText(12, 24, "PERFECT!"); - else if ((shot_diff_x > -4) && (shot_diff_x < 4) && (shot_diff_y > -4) && (shot_diff_y < 4)) // Aim in range (8px x 8px) - consoleDrawText(14, 24, "Nice."); // inside 8x8 range, displays a good message - else - consoleDrawText(2, 24, "You can do better. Try again?"); // More than 8x8, we think is too much + consoleDrawText(9, 24, "Are you ready?"); if (pause_adjust) { - consoleDrawText(4, 25, "Press PAUSE to return"); + consoleDrawText(5, 25, "Press PAUSE to return"); consoleDrawText(3, 26, "Press CURSOR to adjust aim"); } else @@ -336,8 +326,8 @@ int main(void) } // We also have to setup Fire rate - scope_holddelay = max_rate; // How much time we need to consider a button has been held - scope_repdelay = max_rate; // Fire rate + scope_holddelay = 60; // We wait a secont until we want to consider that the button has been held + scope_repdelay = slow_fire_rate; // Fire rate setFadeEffect(FADE_OUT); @@ -365,7 +355,7 @@ int main(void) consoleDrawText(8, 1, " "); consoleDrawText(1, 3, " "); consoleDrawText(8, 4, " "); - consoleDrawText(1, 24, " "); + consoleDrawText(9, 24, " "); consoleDrawText(4, 25, " "); consoleDrawText(3, 26, " "); @@ -373,7 +363,7 @@ int main(void) setPaletteColor(17, 0x0000); // and black for texts WaitForVBlank(); - dmaFillVram(&cali_target_map, 0x2800, 0x700); // Wipe screen + dmaFillVram(&aim_target_map, 0x2800, 0x700); // Wipe screen oamInitGfxAttr(0x0000, OBJ_SIZE32_L64); // Set to bigger sprites setScreenOn(); @@ -421,14 +411,33 @@ int main(void) if (enable_fire) { - if (scope_held & SSC_FIRE) + if (scope_held & SSC_FIRE) // We're using scope_held to swap from a normal fire rate to a slower firerate + if (cool_down < 2) + cool_down++; + + if ((scope_down & SSC_FIRE) == 0) + cool_down = 0; + + if (((scope_held & SSC_FIRE) && (cool_down == 2)) || ((scope_down & SSC_FIRE) && (ready_to_fire == normal_fire_rate))) { bullet_id++; if (bullet_id == 32) bullet_id = 0; - bullet_x[bullet_id] = scope_shoth - 0x1A; - bullet_y[bullet_id] = scope_shotv - 0x14; + if (scope_shoth < 0x110) + { + bullet_x[bullet_id] = scope_shoth - 0x10; + if (scope_shotv < 0xE0) + bullet_y[bullet_id] = scope_shotv - 0x10; + else + bullet_y[bullet_id] = 0xE0; + } + else + { + bullet_x[bullet_id] = 0x110; + bullet_y[bullet_id] = 0xE0; + } + bullet_frame[bullet_id] = 0; bullet_draw[bullet_id] = 0; @@ -440,8 +449,17 @@ int main(void) difuse_y[bullet_id] = ((rand() & 0xF0) >> 4) - 8; shot_bullets++; // incease shot bullets by one + ready_to_fire = 0; } + if (cool_down < 2) + { + if (ready_to_fire < normal_fire_rate) + ready_to_fire++; // This will prevent lag if we shoot too fast + } + else + ready_to_fire = 0; + for (bullet_num = 0; bullet_num < shot_bullets; bullet_num++) { s8 id = bullet_id - bullet_num; @@ -495,7 +513,7 @@ int main(void) consoleDrawText(13, 13, " "); consoleDrawText(10, 14, " "); - setPalette(&cali_target_palette, 0, 2); // Restore blue BACK + setPalette(&aim_target_palette, 0, 2); // Restore blue BACK setPaletteColor(17, 0x7FFF); // Restore white text hideSprites(); @@ -596,7 +614,7 @@ int main(void) setFadeEffect(FADE_OUT); resetGame(); - setPalette(&cali_target_palette, 0, 2); // Restore blue BACK + setPalette(&aim_target_palette, 0, 2); // Restore blue BACK setPaletteColor(17, 0x7FFF); // Restore white text goto START_OVER; } diff --git a/snes-examples/input/superscope/SuperScope.h b/snes-examples/input/superscope/SuperScope.h index 9fd63b5e..5ed49a85 100644 --- a/snes-examples/input/superscope/SuperScope.h +++ b/snes-examples/input/superscope/SuperScope.h @@ -1,7 +1,8 @@ #ifndef SUPERSCOPE_H #define SUPERSCOPE_H -#define max_rate 10 // To put some limit to turbo mode, so we're gonna fire every 10 frames +#define normal_fire_rate 10 // To put some limit to turbo mode, so we're gonna fire every 10 frames +#define slow_fire_rate 20 // To put some limit to turbo mode, so we're gonna fire every 20 frames #define max_bullets 32 #define max_targets 64 @@ -10,6 +11,8 @@ bool sscope_disconnected = false; // if SuperScope is disconnected during gamepl bool pause_adjust = false; // If we pause during game, will lead us to calibration screen, but we will be able to continue our game // Let's declare some usefull variables to animate our fantastic blue bullets :D +u8 ready_to_fire = 0; +u8 cool_down = 0; u8 bullet_id = 0; // bullet id, we're going to use a maximum of 32 bullets, and then they reset to 0 u8 bullet_num = 0; // bullet number, this will help us to draw multiple bullets on screen at the same time u8 shot_bullets = 0; // the number of bullets we have shot and need to be drawn on screen. Every time a bullet dissapears, this number will decrease by one. diff --git a/snes-examples/input/superscope/calibration_target.png b/snes-examples/input/superscope/aim_adjust_target.png similarity index 100% rename from snes-examples/input/superscope/calibration_target.png rename to snes-examples/input/superscope/aim_adjust_target.png diff --git a/snes-examples/input/superscope/data.asm b/snes-examples/input/superscope/data.asm index f26fc6b4..e8b36117 100644 --- a/snes-examples/input/superscope/data.asm +++ b/snes-examples/input/superscope/data.asm @@ -16,15 +16,15 @@ sprites_palette: .incbin "sprites.pal" - cali_target_tileset: - .incbin "calibration_target.pic" + aim_target_tileset: + .incbin "aim_adjust_target.pic" - cali_target_tileset_end: + aim_target_tileset_end: - cali_target_map: - .incbin "calibration_target.map" + aim_target_map: + .incbin "aim_adjust_target.map" - cali_target_palette: - .incbin "calibration_target.pal" + aim_target_palette: + .incbin "aim_adjust_target.pal" .ends \ No newline at end of file diff --git a/snes-examples/input/superscope/hdr.asm b/snes-examples/input/superscope/hdr.asm index 99d834a6..5c229e62 100644 --- a/snes-examples/input/superscope/hdr.asm +++ b/snes-examples/input/superscope/hdr.asm @@ -15,7 +15,7 @@ .SNESHEADER ID "SNES" ; 1-4 letter string, just leave it as "SNES" - NAME "LIBSNES S.SCOPE INPUT" ; Program Title - can't be over 21 bytes, + NAME "LIBSNES SUPER SCOPE I" ; Program Title - can't be over 21 bytes, ; "123456789012345678901" ; use spaces for unused bytes of the name. SLOWROM From bce77f3f9235871c0cae335c08fa4bd3294bdf8e Mon Sep 17 00:00:00 2001 From: Carlos O'Connor Date: Wed, 10 Apr 2024 01:53:27 -0500 Subject: [PATCH 045/106] SMCONV fix soundbank.asm now fill banks with HiROM mapping, following -i usage. This is necessary or sound will be broken. --- tools/smconv/it2spc.cpp | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/tools/smconv/it2spc.cpp b/tools/smconv/it2spc.cpp index 61e16f71..fdbf6a9b 100644 --- a/tools/smconv/it2spc.cpp +++ b/tools/smconv/it2spc.cpp @@ -1008,7 +1008,14 @@ namespace IT2SPC "\n", size); - if (size <= 32768) + int banksize; + + if (HiROM) + banksize = 65536; + else + banksize = 32768; + + if (size <= banksize) { fprintf(f, ".BANK %i\n" @@ -1035,7 +1042,7 @@ namespace IT2SPC } else { - u32 lastbank = size / 32768; + u32 lastbank = size / banksize; for (u32 j = 0; j <= lastbank; j++) { fprintf(f, @@ -1057,12 +1064,22 @@ namespace IT2SPC // if( ffo != std::string::npos ) // foo = foo.substr( ffo + 1 ); - if (j == 0) - fprintf(f, ".incbin \"%s\" read $8000\n", foo.c_str()); - else if (j == lastbank) - fprintf(f, ".incbin \"%s\" skip $%x\n", foo.c_str(), j * 0x8000); - else - fprintf(f, ".incbin \"%s\" skip $%x read $8000\n", foo.c_str(), j * 0x8000); + if (HiROM) { + if (j == 0) + fprintf(f, ".incbin \"%s\" read $10000\n", foo.c_str()); + else if (j == lastbank) + fprintf(f, ".incbin \"%s\" skip $%x\n", foo.c_str(), j * 0x10000); + else + fprintf(f, ".incbin \"%s\" skip $%x read $10000\n", foo.c_str(), j * 0x10000); + } + else { + if (j == 0) + fprintf(f, ".incbin \"%s\" read $8000\n", foo.c_str()); + else if (j == lastbank) + fprintf(f, ".incbin \"%s\" skip $%x\n", foo.c_str(), j * 0x8000); + else + fprintf(f, ".incbin \"%s\" skip $%x read $8000\n", foo.c_str(), j * 0x8000); + } fprintf(f, ".ENDS\n\n"); } From ffc3ae0ac685d6a2001c230e87afdece4fd684c6 Mon Sep 17 00:00:00 2001 From: Carlos O'Connor Date: Sat, 13 Apr 2024 16:50:56 -0500 Subject: [PATCH 046/106] Music inside HiROM mapped rom example soundbank is smaller than $10000 in size, so it fits in one single bank. Musical theme rearranged and some fun animations have been added ;) --- snes-examples/audio/musicHiROM/Makefile | 38 +++ snes-examples/audio/musicHiROM/dancer.png | Bin 0 -> 5915 bytes snes-examples/audio/musicHiROM/data.asm | 19 ++ snes-examples/audio/musicHiROM/hdr.asm | 49 +++ snes-examples/audio/musicHiROM/musicHiROM.c | 308 ++++++++++++++++++ .../audio/musicHiROM/pvsneslibfont.bmp | Bin 0 -> 7222 bytes .../audio/musicHiROM/res/whatislove.it | Bin 0 -> 214799 bytes 7 files changed, 414 insertions(+) create mode 100644 snes-examples/audio/musicHiROM/Makefile create mode 100644 snes-examples/audio/musicHiROM/dancer.png create mode 100644 snes-examples/audio/musicHiROM/data.asm create mode 100644 snes-examples/audio/musicHiROM/hdr.asm create mode 100644 snes-examples/audio/musicHiROM/musicHiROM.c create mode 100644 snes-examples/audio/musicHiROM/pvsneslibfont.bmp create mode 100644 snes-examples/audio/musicHiROM/res/whatislove.it diff --git a/snes-examples/audio/musicHiROM/Makefile b/snes-examples/audio/musicHiROM/Makefile new file mode 100644 index 00000000..a16ee24c --- /dev/null +++ b/snes-examples/audio/musicHiROM/Makefile @@ -0,0 +1,38 @@ +ifeq ($(strip $(PVSNESLIB_HOME)),) +$(error "Please create an environment variable PVSNESLIB_HOME by following this guide: https://github.com/alekmaul/pvsneslib/wiki/Installation") +endif + +HIROM := 1 + +# BEFORE including snes_rules : +# list in AUDIOFILES all your .it files in the right order. It will build to generate soundbank file +AUDIOFILES := res/whatislove.it +# then define the path to generate soundbank data. The name can be different but do not forget to update your include in .c file ! +export SOUNDBANK := res/soundbank + +include ${PVSNESLIB_HOME}/devkitsnes/snes_rules + +.PHONY: bitmaps all + +#--------------------------------------------------------------------------------- +# ROMNAME is used in snes_rules file +export ROMNAME := musicHiROM + +# to build musics, define SMCONVFLAGS with parameters you want, for HiROM use -i +SMCONVFLAGS := -s -o $(SOUNDBANK) -i -V -b 3 +musics: $(SOUNDBANK).obj + +all: musics bitmaps $(ROMNAME).sfc + +clean: cleanBuildRes cleanRom cleanGfx cleanAudio + +#--------------------------------------------------------------------------------- +pvsneslibfont.pic: pvsneslibfont.bmp + @echo convert font with no tile reduction ... $(notdir $@) + $(GFXCONV) -s 8 -o 2 -u 16 -p -e 1 -t bmp -i $< + +dancer.pic: dancer.png + @echo convert font with no tile reduction ... $(notdir $@) + $(GFXCONV) -s 32 -o 4 -u 16 -p -t png -i $< + +bitmaps : pvsneslibfont.pic dancer.pic diff --git a/snes-examples/audio/musicHiROM/dancer.png b/snes-examples/audio/musicHiROM/dancer.png new file mode 100644 index 0000000000000000000000000000000000000000..77a12a9d9d2579ecb93df730e94975cb34dde070 GIT binary patch literal 5915 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 gFGRW0Xyq$FZ=ym32k) demo +---------------------------------------------------------------------------------*/ +#include + +#include "res/soundbank.h" + +#define max_colors 6 + +extern char snesfont, snespal, dancermap, dancerpal; + +// soundbank that are declared in soundbank.asm +extern char SOUNDBANK__; + +unsigned short color_table[] = {0x001F, 0x03FF, 0x03E0, 0x7FE0, 0x7C00, 0x7C1F}; +char lyrics; + +s8 flashcolor = 0x1F; +u8 bgcolor = 0x1F; +u16 bgsolidcolor = 0x1F; + +u8 set; +u8 dance_0 = 0; +u8 dance_1 = 0; +u8 dance_anim = 32; +u8 dance_anim_set_0 = 0; +u8 dance_anim_set_1 = 0; +u8 dancer_x = 127; +u8 dancer_dir = 1; +u16 dancer_pose_cnt = 0; +u8 dancer_pose = 0; +u8 lyrics_part = 0; +u16 lyrics_cycle = 0; +u8 bg_inc = 0; +u8 bgscolsel = 0; + +//--------------------------------------------------------------------------------- +int main(void) +{ + // Initialize sound engine (take some time) + spcBoot(); + + // Initialize SNES + consoleInit(); + + // Initialize text console with our font + consoleSetTextVramBGAdr(0x6800); + consoleSetTextVramAdr(0x3000); + consoleSetTextOffset(0x0100); + consoleInitText(0, 16 * 2, &snesfont, &snespal); + + // Set soundbank available in soundbank.asm. + spcSetBank(&SOUNDBANK__); + + // Load music. Constant is automatically defined in soundbank.h + spcLoad(MOD_WHATISLOVE); + + // Init background + bgSetGfxPtr(0, 0x2000); + bgSetMapPtr(0, 0x6800, SC_32x32); + + // Now Put in 16 color mode and disable Bgs except current + setMode(BG_MODE1, 0); + bgSetDisable(1); + bgSetDisable(2); + + // Draw a wonderful text :P + consoleDrawText(2, 8, "Let the music play inside a"); + consoleDrawText(4, 10, "HiROM memory mapped ROM!"); + + // Let's dance with our dancer + oamInitGfxSet(&dancermap, 0x1800, &dancerpal, 4 * 2, 0, 0x0000, OBJ_SIZE32_L64); + + // Syng with program + spcFlush(); + + // Play file from the beginning + spcPlay(0); + + // Wait for nothing :P + setScreenOn(); + + // Let's dance!!! + while (1) + { + // Update music / sfx stream and wait vbl + spcProcess(); + WaitForVBlank(); + + if (dance_0 < 62) // let's wait a bit first to sync with music + dance_0++; + else + { + // Let's animate our dancers + oamSet(0, (dancer_x >> 3) + 0x40, 0x90, 0, dancer_dir, 0, (dance_anim << 2), 0); + oamSet(4, (dancer_x >> 3) + 0x60, 0x90, 0, dancer_dir, 0, (dance_anim << 2), 0); + oamSet(8, (dancer_x >> 3) + 0x80, 0x90, 0, dancer_dir, 0, (dance_anim << 2), 0); + + dance_anim_set_0++; + if (dancer_pose) + { + if (dance_anim_set_1 == 0) + set = 3; + else + set = 4; + if (dance_anim_set_0 > set) + { + dance_anim++; + dance_anim_set_0 = 0; + dance_anim_set_1++; + if (dance_anim_set_1 > 4) + dance_anim_set_1 = 0; + } + + switch (dance_anim) + { + case 4: + dance_anim = 16; + break; + case 18: + dance_anim = 0; + break; + } + + if (dancer_dir) + dancer_x++; + else + dancer_x--; + + if (dancer_x == 255) + dancer_dir = 0; + if (dancer_x == 0) + dancer_dir = 1; + } + else + { + if (dance_anim_set_1 == 0) + set = 7; + else + set = 6; + + if (dance_anim_set_0 > set) + { + dance_anim++; + dance_anim_set_0 = 0; + dance_anim_set_1++; + if (dance_anim_set_1 > 4) + dance_anim_set_1 = 0; + } + + if (dance_anim == 36) + dance_anim = 32; + } + + dancer_pose_cnt++; + if (dancer_pose_cnt > 460) + { + dancer_pose = !dancer_pose; + + if (dancer_pose) + dance_anim = 0; + else + dance_anim = 32; + dancer_pose_cnt = 0; + bg_inc = 29; + } + + // change background color + bg_inc++; + flashcolor -= 4; + if (flashcolor < 0) + flashcolor = 0x00; + if (flashcolor == 0) + bgcolor--; + + if (bg_inc > 28) + { + bgcolor = 0x1F; + flashcolor = 0x1F; + bgscolsel++; + if (bgscolsel == max_colors) + bgscolsel = 0; + bgsolidcolor = color_table[bgscolsel]; + bg_inc = 0; + } + + setPaletteColor(0x00, bgsolidcolor & (bgcolor | (bgcolor << 5) | (bgcolor << 10)) | (flashcolor | (flashcolor << 5) | (flashcolor << 10))); + } + + if (dance_1 < 22) // let's wait a bit first to sync with music + dance_1++; + else + { + lyrics_cycle++; + switch (lyrics_cycle) + { + case 1: + switch (lyrics_part) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 10: + case 11: + case 13: + case 14: + case 15: + consoleDrawText(10, 24, "What is love?"); + break; + } + break; + case 81: + consoleDrawText(10, 24, " "); + break; + case 96: + switch (lyrics_part) + { + case 0: + case 4: + case 5: + case 10: + case 11: + case 14: + case 15: + consoleDrawText(6, 24, "Baby, Don't hurt me"); + break; + case 6: + case 7: + consoleDrawText(8, 24, "Uoououoouooohoo!"); + break; + } + break; + case 124: + if (lyrics_part == 17) + consoleDrawText(10, 24, "Don't hurt me"); + break; + case 196: + consoleDrawText(6, 24, " "); + break; + case 231: + switch (lyrics_part) + { + case 0: + case 4: + case 5: + case 10: + case 11: + case 14: + case 15: + consoleDrawText(10, 24, "Don't hurt me"); + break; + } + break; + case 259: + switch (lyrics_part) + { + case 6: + case 7: + consoleDrawText(10, 24, "Uoououooohoo!"); + break; + } + break; + case 311: + consoleDrawText(10, 24, " "); + break; + case 346: + switch (lyrics_part) + { + case 0: + case 4: + case 5: + case 10: + case 11: + case 14: + case 15: + consoleDrawText(12, 24, "No more!"); + break; + case 17: + consoleDrawText(10, 24, "Don't hurt me"); + break; + } + break; + case 374: + switch (lyrics_part) + { + case 6: + case 7: + consoleDrawText(12, 24, "uuuhuuu!"); + break; + } + break; + case 426: + consoleDrawText(10, 24, " "); + break; + case 461: + lyrics_cycle = 0; + lyrics_part++; + if (lyrics_part == 18) + lyrics_part = 1; + break; + } + } + } + return 0; +} \ No newline at end of file diff --git a/snes-examples/audio/musicHiROM/pvsneslibfont.bmp b/snes-examples/audio/musicHiROM/pvsneslibfont.bmp new file mode 100644 index 0000000000000000000000000000000000000000..6fba7488608d5d0d02e9b49dceb749c6ac246807 GIT binary patch literal 7222 zcmeHIL6Q?e44V`jcmf9=zzg;P-1+}=7E7|+cJEA2t7xQudmZm|fFNpC_bglCtRYse5q z;rjsZpYS5pSer!L{T4|{S>~&We9U9|#YBjDO}k`nxE8r|!c!>^Z<#Mr?F)ZAonjDITw= zN)UY$(DgK_4M&;_6tOJ0$7-wW8x<2tvGZ~QEYiPkwTNMrNR(@@gk+_K#{?DW$Ucz* zP|=cyix!oxxvm|Xu0iH=4v*GEjTh*q*WPR2ze(wtohN6 z!sLNNn2mu8%Z?~fxMoCMzHVMcFKDI5qFx$aAHet>)YwYcw?$d=1?x!6Je42j$d4~w zOpLAf;S~LNxE3Qxk_u$=zpAAI-Ey1cyVV*Z_?*-ny9}xgD7yOTbAk}>9}xcP0Vob@2(^_wSf?B< zq9!<2e@{U0XBlG7+Wr|NIuHqTo5}&@Y3k}<7CurUg1GZBPLO-ntZ-! z+kJ=zC~9KgW8ms*LUP^5jhI((SrHWd#2Hu;;{NFec6#bQ;0hxQ3*Fac*5WL`vOh^6 ztYOu%LKf<$fqnLv3kc?kBR&~EjWD3DC9DNjYxfi#bM)E!5T1RYi-*2G z_Sn6|^p0d2Dj#(9z0rH(`E{^g-wTp#Bs+H?p4~EYA_=RcM}FeJ^vZZ*0VI$M);$8T z*>y|-l+bTcD*$B+had6d9Nx)nZfV;PI(VGz9*&;hF20Q0t+Y7--KKy?MxGglzQgc! zt#SPQB2|ZH9wJ0Rjb2Xh{8my@IE}>>Wx0-;yg+vSbMq$zb d%ZJ_8suW2TxPX10iqFeaxd&_)o8Ubi#~)~B4uAjv literal 0 HcmV?d00001 diff --git a/snes-examples/audio/musicHiROM/res/whatislove.it b/snes-examples/audio/musicHiROM/res/whatislove.it new file mode 100644 index 0000000000000000000000000000000000000000..453385cb49830580dc53ff546a21f90fb4fd57b7 GIT binary patch literal 214799 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

G-J?&oUWU1onV5&O2JIaO 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

I}g+N8Arr*#UjN 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 literal 0 HcmV?d00001 From e07033f22b4ac817d496323288351ad0c2ef8379 Mon Sep 17 00:00:00 2001 From: Carlos O'Connor Date: Sat, 13 Apr 2024 20:02:46 -0500 Subject: [PATCH 047/106] Bug fixes for mouse input and example I removed a branch present in bios, so both ports are read in one single vblank interrupt. detectMouse() is called later, after nmi, so mouse is detected. snes_mouse is set to 0 when both mouses are not found, not the second one, this is to prevent potential issues. --- pvsneslib/source/input.asm | 12 +++++++----- snes-examples/input/mouse/mouse.c | 17 ++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/pvsneslib/source/input.asm b/pvsneslib/source/input.asm index 07d07f7f..bc3aea79 100644 --- a/pvsneslib/source/input.asm +++ b/pvsneslib/source/input.asm @@ -703,12 +703,9 @@ _10: jsr speed_change stz connect_st+1 - bra _30 - _20: dex lda REG_JOY1L ; Joy1 - jsr mouse_data lda connect_st @@ -718,6 +715,13 @@ _20: stz connect_st _30: + + lda mouseConnect + ora mouseConnect+1 + bne + + stz snes_mouse ; Disable mouse flag if no mouse connected + ++: ply plx plb @@ -738,8 +742,6 @@ mouse_data: stz mouse_x,x stz mouse_y,x - stz snes_mouse - rts _m10: diff --git a/snes-examples/input/mouse/mouse.c b/snes-examples/input/mouse/mouse.c index d812b121..8329857d 100644 --- a/snes-examples/input/mouse/mouse.c +++ b/snes-examples/input/mouse/mouse.c @@ -43,12 +43,6 @@ int main(void) // Initialize SNES consoleInit(); - // we set mouse speed, or it will just output a random speed. We can change it later manually - mouseSpeedSet[0] = slow; - mouseSpeedSet[1] = slow; - - detectMouse(); // Let's check if a mouse is plugged in any port on boot - // Init cursors sprite oamInitGfxSet(&cursorsprite, (&cursorsprite_end - &cursorsprite), &cursorpal, 48 * 2, 0, 0x0000, OBJ_SIZE16_L32); @@ -61,7 +55,12 @@ int main(void) // Draw a wonderful text :P consoleDrawText(11, 1, "MOUSE TEST"); - WaitForVBlank(); // Let's make sure we read mouse for the first time + // we set mouse speed, or it will just output a random speed. We can change it later manually + mouseSpeedSet[0] = slow; + mouseSpeedSet[1] = slow; + + detectMouse(); // Let's check if a mouse is plugged in any port on boot, be sure nmi interrupt was called at least once (in this case, previous oamInitGfxSet() function was enough) + WaitForVBlank(); // Let's make sure we read mouse for the first time after detectMouse() if (mouseConnect[0] == false) consoleDrawText(3, 5, "NO MOUSE PLUGGED ON PORT 0"); @@ -71,7 +70,7 @@ int main(void) dmaCopyVram(&buttonsmap + 0xA0, 0x61A8, 0x0A); // SLOW button pressed dmaCopyVram(&buttonsmap + 0x4A, 0x618D, 0x16); // released buttons dmaCopyVram(&buttonsmap + 0x8A, 0x61AD, 0x16); // released buttons - consoleDrawText(4, 5, "MOUSE PLUGGED ON PORT 0"); + consoleDrawText(5, 5, "MOUSE PLUGGED ON PORT 0"); } if (mouseConnect[1] == false) @@ -82,7 +81,7 @@ int main(void) dmaCopyVram(&buttonsmap + 0xA0, 0x6328, 0x0A); // SLOW button pressed dmaCopyVram(&buttonsmap + 0x4A, 0x630D, 0x16); // released buttons dmaCopyVram(&buttonsmap + 0x8A, 0x632D, 0x16); // released buttons - consoleDrawText(4, 17, "MOUSE PLUGGED ON PORT 1"); + consoleDrawText(5, 17, "MOUSE PLUGGED ON PORT 1"); } // Init background From 69a5fa1898d077deb2797cc4e76163a72d21ab5c Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 14 Apr 2024 07:02:39 +0200 Subject: [PATCH 048/106] chore(*): convert to 16 colors --- snes-examples/hello_world/pvsneslibfont.png | Bin 1739 -> 1014 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/snes-examples/hello_world/pvsneslibfont.png b/snes-examples/hello_world/pvsneslibfont.png index 043dbf23ccfbd05ecb4cf639ace4ee8efffdcbac..ae8ba309232920ba95bc17862101296c00ba9a3f 100644 GIT binary patch delta 893 zcmV-@1A_d^4fY3+7#sux0002I6opLy000McNliru=mZW00SqIP&-UuBF@lg?;o?2 z^VeOCWuMs3jr-y`-|;20l;nQK3o(sLs(Jc{bH1OGaG2;D=walf|@8IK(y7Mfy4Y!f6+ZOy%Vp!`pnCx_7Zzp zr0Q$$UWYY`w(al14eRNjyw-JnN)KnnyI-4>2A<|PRwBG2U=-=`Sn;wu;{CJNx($B^ zzSS`uV$DSA9q(FabsdX4Z(SwszSUF&bZb>F`aVOW)9PD$RbRc;xoh7yJy)*yB0`^Y zuGY^%+x4HQd}!qQ+VFRM>AhWF7TlQd+{;WNhV;uG8=*jLo3i<{_*TI!f2$XHMCwhY zCLZ+zBMr{6=y`KYV9^-9&G^nMf6;$~L=SB}Gq$>IMPCjxed41p{JlcIWH)Z9&hPpN zUaf;+_bbbgv*bZ`y-O~)?d~P6Va37r3^R`UJ54cmXV=&dgA2n{1%Eefv)yYmH+XHs zS*0&E_ z_Bnhk#G{57z9b|6pXVDl=P$uR_};^3k$=Fp()lNcYioP^0nG*d`yA@DP{jWLw~HSC TCAue)00000NkvXXu0mjfQf<8; literal 1739 zcmeAS@N?(olHy`uVBq!ia0y~yUFdh=jFX>*Uw6)z6Lml# z@eXI(MdpERDYNG( zM|+>;`7ue=IjC!G?(F@46@I_xT=ack;J$aS{+D_FKg;*befzSz$0z(N+s-j#%l(4) z#*FnhzvVN*IwF#>JxcYUaDAN_Y>+wlhF4lcl zIPz}KyC}VDp_U!@CH|HS4<^CV%a^r2v^}@tGy3)FxZuoVxw4g0Q)FZomc-wftzn`f zf60d_t^Lo$5}C~9FSi)vI`==k#P?(63U)i0M>X%vD=K%dDQ&u={Pfkae~;vfY_~+~MW7N^`-=JLo#Ov+9Z7S@t@AAYxyB)Xg^B()ZSNPR)=X9{Iu#RmyFMZonPU}@f zt>^osm(%1WZN8=(e`EH!$h2tlp9A+FtNi4AXPJBGdEa@?sV@0% zhKH8F_P;*i?q%yAb#cO)FZ{F43;*0(!R{oOUJ!fkRp0Bl!#aPB-$Xp$GvmP-?#-<4 zrP^O#YP;qBn6<3Y;kIC;(5EfFtGBvJ?TYW)ezhMC^leypkg=%g`KVV?WWHUH1Q zKD+NB|F34g|6#YLfBw7a`q#MyKguUhuW$1@S$}cww9O^=8|9xy8vfd0x!D|4DtNm3 KxvX Date: Sun, 14 Apr 2024 07:05:42 +0200 Subject: [PATCH 049/106] chore(*): fix typo --- tools/gfx4snes/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/gfx4snes/readme.md b/tools/gfx4snes/readme.md index ca7f5b0c..107ff940 100644 --- a/tools/gfx4snes/readme.md +++ b/tools/gfx4snes/readme.md @@ -11,7 +11,7 @@ ``` A graphic converter for Super Nintendo development -Developed by ALekmaul and distributed under the terms of the [MIT license](./LICENSE). +Developed by Alekmaul and distributed under the terms of the [MIT license](./LICENSE). ## Usage ``` From 9162f9a795475e3dedda2f65f3ebff54b9667607 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 14 Apr 2024 07:05:59 +0200 Subject: [PATCH 050/106] chore(*): change version --- tools/gfx4snes/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/gfx4snes/Makefile b/tools/gfx4snes/Makefile index e4c880c6..e582ac18 100644 --- a/tools/gfx4snes/Makefile +++ b/tools/gfx4snes/Makefile @@ -1,5 +1,5 @@ # Version number and build date string -VERSION := 1.0.0 +VERSION := 1.0.1 DATESTRING := $(shell date +%Y%m%d) # Compiler and compiler flags From b1a01d2c41259aff58ad767972791f33aa84abcb Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 14 Apr 2024 07:06:39 +0200 Subject: [PATCH 051/106] fix(colors): change color converison, fix #275 --- tools/gfx4snes/src/images.c | 79 +------------------------------------ 1 file changed, 1 insertion(+), 78 deletions(-) diff --git a/tools/gfx4snes/src/images.c b/tools/gfx4snes/src/images.c index c1a1029e..8ecf9b0b 100644 --- a/tools/gfx4snes/src/images.c +++ b/tools/gfx4snes/src/images.c @@ -34,54 +34,6 @@ t_image snesimage={0}; // current image converted -#if 0 -// Un-bitpacks a source image into an 8-bit-per-pixel destination buffer -// Acceptable range is 1-8 bits per pixel -static void image_bitunpack(const vector & src_image_data, vector & unpacked_image_data, uint8_t bitdepth) { - - int pixels_per_byte = (8 / bitdepth); - - // Loop through all pixels in source image buffer - for (const uint8_t & it_src: src_image_data) { - - // Unpack grouped pixel data - uint8_t packed_bits = it_src; - for (int b = 0; b < pixels_per_byte; b++) { - unpacked_image_data.push_back(packed_bits >> (8 - bitdepth)); // Save extracted pixel bits - packed_bits <<= bitdepth; // Shift out the extracted bits - } - } -} - - -// Converts indexed image with bit depths 1- 8 bpp to indexed 8 bits per pixel -// Replaces incoming image buffer with unpacked image buffer if successful -bool image_indexed_ensure_8bpp(vector & src_image_data, int width, int height, int bitdepth, int colortype) { - - vector unpacked_image_data; - - if (colortype != LCT_PALETTE) { - printf("png2asset: Error: keep_palette_order only works with indexed png images (pngtype: %d, bits:%d)\n", - colortype, bitdepth); - return false; - } - else if ((bitdepth < 1) || (bitdepth > 8)) { - printf("png2asset: Error: Indexed mode PNG requires between 1 - 8 bits per pixel (pngtype: %d, bits:%d)\n", - colortype, bitdepth); - return false; - } - - image_bitunpack(src_image_data, unpacked_image_data, bitdepth); - - // Replace source image pixel data with unpacked pixels - src_image_data.clear(); - for (const uint8_t & it_unpacked: unpacked_image_data) - src_image_data.push_back(it_unpacked); - - return true; -} - -#endif //------------------------------------------------------------------------------------------------- void image_load_png(const char *filename, t_image *img, bool isquiet) { @@ -102,12 +54,11 @@ void image_load_png(const char *filename, t_image *img, bool isquiet) } sprintf(outputname,"%s.png",filename); -#if 1 // optionally customize the state lodepng_state_init(&pngstate); // no conversion of color (to keep palette mode) - pngstate.decoder.color_convert = 0; + pngstate.decoder.color_convert = true; // always in 8 bits and palette mode pngstate.info_raw.colortype=LCT_PALETTE; @@ -119,35 +70,7 @@ void image_load_png(const char *filename, t_image *img, bool isquiet) { pngerror = lodepng_decode(&pngimage, &pngwidth, &pngheight, &pngstate, pngbuff, pngsize); } -#else - // load the png file and try to decode it - pngerror = lodepng_load_file(&pngbuff, &pngsize, outputname); - if (!pngerror) - { - //Calling with keep_palette_order means - //-The image should be png indexed (1-8 bits per pixel) - //-For CGB: Each 4 colors define a gbc palette, the first color is the transparent one - //-Each rectangle with dimension(tile_w, tile_h) in the image has colors from one of those palettes only - pngstate.info_raw.colortype = LCT_PALETTE; - pngstate.info_raw.bitdepth = 8; - // * Do *NOT* turn color_convert ON here. - // When source PNG is indexed with bit depth was less than 8 bits per pixel then - // color_convert may mangle the packed indexed values. Instead manually unpack them. - // - // This is necessary since some PNG encoders will use the minimum possible number of bits. - // For example 2 colors in the palette -> 1bpp -> 8 pixels per byte in the decoded image. - // Also see below about requirement to use palette from source image - pngstate.decoder.color_convert = false; - pngerror = lodepng_decode(&pngimage, &pngwidth, &pngheight, &pngstate, pngbuff, pngsize); - - //unsigned error = lodepng::decode(image.data, image.w, image.h, state, buffer); - // Unpack the image if needed. Also checks and errors on incompatible palette type if needed - if (! image_indexed_ensure_8bpp(&pngimage, &pngwidth, &pngheight,, (int)pngstate.info_png.color.bitdepth, (int)pngstate.info_png.color.colortype)) { - return 1; - } - } -#endif if (pngerror) { free(pngbuff); From bd4d3c76faad0f4ae484553dbc9e147a614c2cb3 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 14 Apr 2024 07:07:17 +0200 Subject: [PATCH 052/106] chore(*): fix ypo --- wiki/Sounds-and-Musics.md | 44 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/wiki/Sounds-and-Musics.md b/wiki/Sounds-and-Musics.md index 44ac6796..362bf07f 100644 --- a/wiki/Sounds-and-Musics.md +++ b/wiki/Sounds-and-Musics.md @@ -1,10 +1,8 @@ -** to do ** - -PVSneslib uses IT files to play musics, it also uses a specific IT file for sound effects. +PVSneslib uses IT files to play musics, it also uses a specific IT file for sound effects. ## Tools to create IT files -You can use a tool like [openMPT](https://openmpt.org/) to create your song. +You can use a tool like [openMPT](https://openmpt.org/) to create your song. ## Composition rules for your IT files @@ -12,16 +10,16 @@ Impulse tracker format must be used. 8 Channels MAX! Notes can not exceed 128Khz playback rate! The module must be in instrument mode. It must have instruments! -DO NOT USE UNDEFINED INSTRUMENTS, they will not silence the channel, THEY WILL CAUSE MALFUNCTION! +DO NOT USE UNDEFINED INSTRUMENTS, they will not silence the channel, THEY WILL CAUSE MALFUNCTION! -The 64 or less samples must fit within 58K of memory. This is AFTER "BRR" compression. 8-bit samples will be reduced to 9/16 size. 16-bit samples will be reduced to 9/32 size. +The 64 or less samples must fit within 58K of memory. This is AFTER "BRR" compression. 8-bit samples will be reduced to 9/16 size. 16-bit samples will be reduced to 9/32 size. -## Tips from KungFuFurby about samples in IT files +## Tips from KungFuFurby about samples in IT files -When something going wrong for the songs you attempted to convert: +When something going wrong for the songs you attempted to convert: - Sample loop points must be divisible by 16. Loop points not divisible by 16 may not convert correctly. -If you don't use loop points divisible by 16, at least make sure the loop length is divisible by 16. +If you don't use loop points divisible by 16, at least make sure the loop length is divisible by 16. I use Schism Tracker to perform the job of loop point analysis and loop point adjustments since I can just simply type in the numbers. @@ -40,26 +38,26 @@ That's a loop length of 169. I like using powers of 2 for my loop points so that if I have to decrease the quality of the sample, then I can do so as painlessly as possible to the sample (unless I find the degraded quality to be a bad idea), so that means 128 is the value I use here. -Divide 169 by 128 gets you an unusual decimal number... copy that number. +Divide 169 by 128 gets you an unusual decimal number... copy that number. -Now get the full length of the sample (that's 3383 for this sample) and divide by that decimal number you acquired earlier (169/128). -You'll most likely get another unusual decimal number. Round that off and there's your new length that you will resize the sample to. +Now get the full length of the sample (that's 3383 for this sample) and divide by that decimal number you acquired earlier (169/128). +You'll most likely get another unusual decimal number. Round that off and there's your new length that you will resize the sample to. -I use Alt-E in Schism Tracker to perform the resize sample command. -The program will ask for the new size of the sample. +I use Alt-E in Schism Tracker to perform the resize sample command. +The program will ask for the new size of the sample. -Now you should have a loop length that is divisible by 16. You can perfect the loop point by adjusting them so that the loop point themselves are divisible by 16. +Now you should have a loop length that is divisible by 16. You can perfect the loop point by adjusting them so that the loop point themselves are divisible by 16. -- Only one sample can be defined per instrument... +- Only one sample can be defined per instrument... -You'd have to duplicate the instruments and then enter the sample ID for all of those notes... and then you have to redefine the instrument IDs depending on the pitch from the old note table. Yeah... +You'd have to duplicate the instruments and then enter the sample ID for all of those notes... and then you have to redefine the instrument IDs depending on the pitch from the old note table. Yeah... -- I thought in one case, I saw the pitch go too high (it went over 128khz). That's because I noticed a hiccup in the notation. +- I thought in one case, I saw the pitch go too high (it went over 128khz). That's because I noticed a hiccup in the notation. -For the one song that has this problem, I usually resize the sample and make it half of its length... -however, I may have to make additional adjustments depending on how the loop points are holding up (length may or may not be involved, although usually I'm checking the loop points themselves to make sure that they are divisible by 32 or some higher power of 16... this indicates how many times you can cut the sample in half). +For the one song that has this problem, I usually resize the sample and make it half of its length... +however, I may have to make additional adjustments depending on how the loop points are holding up (length may or may not be involved, although usually I'm checking the loop points themselves to make sure that they are divisible by 32 or some higher power of 16... this indicates how many times you can cut the sample in half). -- Note Cut is the only New Note Action supported for SNESMod. +- Note Cut is the only New Note Action supported for SNESMod. -One of these songs is the most visibly affected by this problem, and that's because SNESMod doesn't virtually allocate channels. -You have to modify the patterns so that the note off commands go where the note would originally play, and the new note is put on another channel. \ No newline at end of file +One of these songs is the most visibly affected by this problem, and that's because SNESMod doesn't virtually allocate channels. +You have to modify the patterns so that the note off commands go where the note would originally play, and the new note is put on another channel. \ No newline at end of file From 41b1f22a2d4a167cfcbaa08bb65a0cad514c46a9 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 14 Apr 2024 07:26:20 +0200 Subject: [PATCH 053/106] fix(*): fix BG3 priority --- .../Backgrounds/Mode1BG3HighPriority/BG1.bmp | Bin 66614 -> 66614 bytes .../Backgrounds/Mode1BG3HighPriority/BG2.bmp | Bin 66614 -> 66614 bytes .../Backgrounds/Mode1BG3HighPriority/Makefile | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) diff --git a/snes-examples/graphics/Backgrounds/Mode1BG3HighPriority/BG1.bmp b/snes-examples/graphics/Backgrounds/Mode1BG3HighPriority/BG1.bmp index d885484ee7c3821b097a0236e8dceae20a7997ca..862fd8c770a04bee67a775d9e9a65005bb211627 100644 GIT binary patch literal 66614 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 Date: Sun, 14 Apr 2024 09:18:19 +0200 Subject: [PATCH 054/106] fix(*): change names to lowercase --- snes-examples/input/superscope/{SuperScope.c => superscope.c} | 0 snes-examples/input/superscope/{SuperScope.h => superscope.h} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename snes-examples/input/superscope/{SuperScope.c => superscope.c} (100%) rename snes-examples/input/superscope/{SuperScope.h => superscope.h} (100%) diff --git a/snes-examples/input/superscope/SuperScope.c b/snes-examples/input/superscope/superscope.c similarity index 100% rename from snes-examples/input/superscope/SuperScope.c rename to snes-examples/input/superscope/superscope.c diff --git a/snes-examples/input/superscope/SuperScope.h b/snes-examples/input/superscope/superscope.h similarity index 100% rename from snes-examples/input/superscope/SuperScope.h rename to snes-examples/input/superscope/superscope.h From 6f9fb9c11e6ec9f45f3be33cd4cd05609baf32b3 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sun, 21 Apr 2024 08:57:36 +0200 Subject: [PATCH 055/106] feat(*): add fps counter display function --- pvsneslib/include/snes/video.h | 5 +++ pvsneslib/source/videos.asm | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/pvsneslib/include/snes/video.h b/pvsneslib/include/snes/video.h index 1646edf8..d9b67a1d 100644 --- a/pvsneslib/include/snes/video.h +++ b/pvsneslib/include/snes/video.h @@ -455,4 +455,9 @@ void setMode7Scale(u16 xscale, u16 yscale); */ unsigned short getFPScounter(void); +/*! \fn showFPScounter(void) + \brief Show on current text BG, at location 1,1 the number of frames per second. +*/ +void showFPScounter(void); + #endif // SNES_VIDEO_INCLUDE diff --git a/pvsneslib/source/videos.asm b/pvsneslib/source/videos.asm index a996b717..0b35e1ed 100644 --- a/pvsneslib/source/videos.asm +++ b/pvsneslib/source/videos.asm @@ -1138,3 +1138,60 @@ _gfctr1: rtl .ENDS + +.SECTION ".videos91_text" SUPERFREE + +;--------------------------------------------------------------------------- +; void showFPScounter(void) +showFPScounter: + php + phb + + jsl getFPScounter ; compute fps + + sep #$20 + + lda.l snes_frame_count + sta.l $4204 + lda.l snes_frame_count+1 ; Write $fps to dividend + sta.l $4205 + LDA #10 ; Write 10 to divisor + sta.l $4206 + nop ; Wait 16 machine cycles + nop + nop + nop + nop + nop + nop + nop + + lda #$80 ; VRAM_INCHIGH | VRAM_ADRTR_0B | VRAM_ADRSTINC_1 set address in VRam for read or write ($2116) + block size transfer ($2115) + sta.l $2115 + rep #$20 + lda.l txt_vram_bg + clc + adc #(1*32+1) ; will put at location 1,1 on character vram BG + sta.l $2116 + + sep #$20 + lda.l $4214 ; A = result low byte ($4215 result high byte) + clc + adc #$10 ; to have number 0 of graphic + sta.l $2118 + lda #$1 + sta.l $2119 + + lda.l $4216 ; A = remainder low byte ($4216 remainder high byte) + clc + adc #$10 ; to have number 0 of graphic + sta.l $2118 + lda #$1 + sta.l $2119 + + plb + plp + rtl + +.ENDS + From 03521c82f33d07997ffdb65cc78ff8759998b8eb Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Wed, 1 May 2024 09:12:38 +0200 Subject: [PATCH 056/106] feat: initial commit --- tools/tiledpalettequant/LICENSE | 21 + tools/tiledpalettequant/carina-nebula.png | Bin 0 -> 84584 bytes tools/tiledpalettequant/enums.js | 32 + tools/tiledpalettequant/index.html | 161 +++ tools/tiledpalettequant/script.js | 319 ++++++ tools/tiledpalettequant/style.css | 27 + tools/tiledpalettequant/worker.js | 1243 +++++++++++++++++++++ 7 files changed, 1803 insertions(+) create mode 100644 tools/tiledpalettequant/LICENSE create mode 100644 tools/tiledpalettequant/carina-nebula.png create mode 100644 tools/tiledpalettequant/enums.js create mode 100644 tools/tiledpalettequant/index.html create mode 100644 tools/tiledpalettequant/script.js create mode 100644 tools/tiledpalettequant/style.css create mode 100644 tools/tiledpalettequant/worker.js diff --git a/tools/tiledpalettequant/LICENSE b/tools/tiledpalettequant/LICENSE new file mode 100644 index 00000000..be19775f --- /dev/null +++ b/tools/tiledpalettequant/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 rilden + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tools/tiledpalettequant/carina-nebula.png b/tools/tiledpalettequant/carina-nebula.png new file mode 100644 index 0000000000000000000000000000000000000000..627494eb80dbac61ab7a884fe6967ce2afe3b8eb GIT binary patch literal 84584 zcmV)5K*_&}P)laSI4XakGlBhUCjGE>Gh@zAWDq#RH0063@-OR_%bTS`X64l<= z%%wp9YSnnQ(Y4GdT6UcJ@b()oxbvQ;1poAONC(0DuSp z#Z$kW07pJow`LlAdIIz5{@4VgUjT&0r%$ODMjZ z<+q8@Rj@b zDLV+DqJ+Mk=JmdEcfQqRSAsyGAYnR+2ca_>>vS6>PI9zPL)J;jFcMC(-PB{Ry?*m! zzus|jW)X@?WL;Jv)IcQYOn=GDq(DXi)jH#I$hO+;t>KG14uwPpHy!> zW5rLO*ng}`Ahe=hl-LXSOtb9tW%2g$HYhNH2L)9GB$AK^L=qK|UvXUSz|P z`q}G8|G(~6fM2|@M~j=v6z4h~0efOXP)p26hsV1J5KpBYZxzU(*>PYBp}-e$u1p9g z7Nwo#!HcSZ<|6*Ufp(*vg64*pQS-FnvyQ38y|C{mFYf)y z{jVS+kp?LMuDW4OvislOdB&-=3rB&1L{Xe7EjKPEle(Uk1nIUp_%Ht6IrVAYhD^Wi$Ck8ak0~NP^?Ro&5ZFU8;Y`B{$5Vq0` zRS9XPn;Aw%@F7@)Cx5;-0wB>&yDOp$0-zB}0T@X|LNOOD6?hUEJ9wny^_hKpYOK>^ zR(zUg{Qy#ljTdd^K?t7OKl${&69M4*OU_gvKw{>SK?ODJ`Vv=*=&hHoiw)N!TGAV@ z9!-Km7)lru%cuYot)MDF;0@=UQL<)Lf3mE`i9v!O8Z=}Y{F+s9Z@I+C-Y;qa3C5to zqxz%MIcOja8d{U{i&+qq@R3_DBme@KoNvGHrcEIFh3`JMWmQFhb{=d=HXr9x7Su}) zAy=QTO46N2ra-$E+D;a@+km_clFoG`&%|0>zk~(IFND)?yK~#l#tTQ=K2P&@Yu#wi zt{>la!3F1L&4sc-RR|&d=)XME%JP+Gt-#3q`bQ6!Fbi2)B@sbT$Z}Mo;R*pG5^4~F zK%@qwhY^7ws25GypkXp722#XEt{D;_s$gXKjQ{cDEX_|Yv z%uFhv1)uADHy@0o>t&=r3MxV6o(H!TLkCBOdsAQ_L!dpz|N6csz|uKaofSpV?r|S5 zQLsPyYm=O=Mk69FYJmrk)fsA3Wg8 z2Tm$LC?3OA+YuFl>*rX^9H;LOo?i2z?fV%6uC7hDuHE18Iqn zK{{zzUhD5IM={{l=WMA&G;w^Y7CVE03ZMPZyI8g8Z)j!tW!J6mNeqAvRIDSZmn;A1p$CO8eeY3K0VE>$=eu_l zGluSYNdV3KcqXDMxq*x!k4#3I`#c4*)<_;;A=e}d^+Xes_qg!EH*Uysd1V|y;KW>L z&!rH;+b=A$q@9CzOMhs3uD-5Tk*qAW}4yXd~gbJ z#DoC}r!o{KK}3eN{ZSeeL32?BDDDi(%&&a!HXAWBIEJ9$ijH{al^eU6#t^bB-#wn9 z%Hi3JRS(a3O9~*&L_|t}jEX=AYH0ajvST8Th-T-y2=Mt2zVXE)O+d~~G(NdlG6W!m z+Vx5ZCz{#g4{lEeMoKGJ8|wfxv3&077tVS5#5*=12oVyJ_h<>rR z_d=j(^O_o{;)azg9Rd|QYx-Be?Avo>E>reJw?&BUpiE!@LQzhH;ut_ZV5-Vk0HF~3 zjARPr%y)fnY!t*2XT|3f9S>_p`wouJDkuUn5){;N&W66n_Z|a?6ckk*0HLUA>O};t zp-?o`8Ec1szH@vqVFg$e>NOAq3{%w(%+8Ie-ne?%xd>mKXm&tkX)QW7-$g<#ZvE^v z!_OU@LL^nRCO&g*@8opz(Ptii^9^s=b7Zn3%EYUOq9YTXwM*&;#@j^LKXCr!%uAw( z$DFj2o#nD*-dpyE9eXK@J8rCrBBA5i{?xpj8FaH`F&bvuGV6YrPyTc(xIHwq7F0 zGXX-h)i|1Oc zZaHmkv3s;>a)>?E7ktfgPnY|==~0XY$^uaUX=Hg&zjRWHvG{4XW4L7}R6-e&A$5U3V z0szODDW`!vQl^*(v5Z(C=HgS9JhSJ7W6phG(8EWMuUN4}!fWXbKw*>#5RgO-VURFV zcM}Vl00bZ`gn~f~Mb)(cy9xvVL!=^Jq1noMYcc%y@;*ccg8%@8;uU5FRWASpL4*o$ z1_cEySehJ?f+&apEOh)(5+MW+(8`sW6UzcnGTionYdgLg&phjM z-@3P$SPsZaTs6!tvbhL32kgKS8Y3Bm+!Nw0mk<5qq2r)=)lj1g8l>>k0tmoyt+x_} z%C4EuwLBZW;Php0Ie+UzI}hIf;0u5OrJXP=QWS~iP*M~9=nHTAosa*JExqshLLfc)^w#|aSt8G%68tQe6H z1}mnBad>%Og$ZvsZ=DASgk7%yG|>o3ghZAA0*sbS(AV)JU;rriY`T*vK*jL#l95m-BLJ@&KsqW_qnUzNSk;ptm%S3R zsmJ!uKX5YvTzksuym*I5nCj#r($44Rng@UR#KVC0b*GOKvnVQ=@BG)E-b(4LQ1#j#?fXw(GU9jjDqV$?Kkztp~sJ z9s;i-go-TaeJA$>`n%VR-hBDe+pgM#0M8w4tt?Ye{q*gdKqxjw(E!rVUYG%-!gk_q zSB^e)AXUKPEKyZUc-KTT0{|u%NsJLW@3d8Wr(33=o=pQ4 zMwUWQOk|QP2Gxd#1N)!5@U$fs@t$ve{qMhdHzG{rP&DOhfvKpdYAg6o-t1-zGkD(8 zo`~217(%z0l`cK^5~32ZGsZBRky`20vu|U>od@oDdhY`Q$QY>LClF+Wf(3JgLcsz| zb$O`A0dO8T3s5Ws5fCj$ypY1pcYI$%eq?<9w3QJ`H}g4wC-k5E*25s#%Lf)g)8Fi! zv)Db_%Enr$4_(%5E)5}Q;qOC@wV9!kx%Z_eAbBtUEoiwT$dRk9A3A4=Srg`sG+3I6 zNiz)J{?U_{ee~z=_+S54s+OElpLGDJ79k?6A4!hQ1e)&_kpR;kGjYwpG2oMry$lAe#(3ngG z&;(!wq~1ylL`n>V3Mhc%hYt#14RB>|i~yF2kg6s3i7$Ln0RHJ)cOl^SK6J~TXCM57 z+pa<~%LmG1NA?ZZOTG{#JqCp!2N)UX^9llJof$iN@@-db@kEzwS(Pbx6M3M9M=i|4 zuUq?kAC<1R!&QM2oVZMYb(vCvkH!?jNWC+}A5g;hQnM-U?JQAo{D+y91Vgf2!ikRiP>rP2C!xM{-yfnXaGJpG} zqr+7kE>U}KG3%yBr)F0TM2)#tr{mce0UU2(H?xONv`MwsfG9Uo_~BEtlI2;N6$HfM z1E}7!y%jty1(^{bmZqc#X)6y<%m8Mh0_Xjj-+a|303q-OJVD4p_`wT}81>l6rWR^e ziVdIaNW|1kVaHhew0@U)2*4^hvk+c)(PkAzR3wDLRL!LXXzqD#j8NxV;fhUzP^u0m zR1gRjDD`Y`=FrKHfB8SBY^kSgfz35rdkFyw2w#8R^0T)rGuA26MO$isgd5hZ55WBu zJJAvWQUw7iAP?PKDwf6$?#rD4Ay5mDIO^sKK!G6hf(TqVf?Nt=RZKxcNh+P; zlACU4#mp^w8jt|CtRC2QU=j<;U<6cPP{g94M3gD0^b%cm>HF_}=&Py#Ohb+)GXW8X zVin}zfuy)@u~|wbw`Avzo)}s=!pH`Mh!wHWQAIA62SBJNb})o1qq>V*jisw$!d9dHE{RB`}5bKTmPjvS5+ zb}~@)t9$j}Og7t2`=X*~raC;-KQTLh$@!a~+HnGiy^F1LsXa#Xej~Q=?addFbFEcrW3{)iq0?(pA8av)QpRKGI zRqd-dLY{4*f`$T(cmCy@ZvE7MFi3xij?Q#TRxaAm^T__j(O>=P#j_Cxd#;EKlo4}u#iIt1mV>V-f?L9 zmBag1tytemrIuLLaB{Xi)EkS)$(i<0f2Cx|t7ZjV6QLt)iw1#H`r_U5DTo&481Xf1 zs1!zde17uHS6uw~i+k7gm-bCHqhb?Eq#!88xfNO{APZowAjOOGS*RFnoH)?CVxx)# zfGvaNV+-Aip;aTbmya)6L?Bw#pB$d*1O;Q*D*+NH5EdUv7ReEn+0+i<*`42f1d6r! zf;%U>2M5i5)l4NL&_*{aSq&PhiJ9+6q3f}LvMB=7C_uu*h)$7^9U2z`6iFz)I^_h@r2SJ5j@>3?OSbz;#gG7$>W6qOUn6dtJ%%3+q~qS zC-%n%+F4Ets^RQ){VyJx142XqAO%(d3972jNSeuI8!At}vUvH%!|aH=E< z#l^AM1gHuEzS{k&uzuBgi&~56rpqovfRVnW_%<5puUvQW=B}4q!8SV{Fd#NXy#Nxv z+WlDc9TUx@0m%eF4~6;y7^n|E^qW@zaQk?}5FtUO6rq3t!U74vlnZ!;G=Kn|xpqVW z>W*`O)oV|If>#~$zPWTJ4Re`DTtP$yQi0-USg>Zat_oXL)+){x_k@6?K#3z!FG4F= z;J?@Vw$=wqmXHBi0lH~-X`dlfMyfay0(eD4Ms`S!5UG$uQyqS-WQZ4>HjGG!Y?yxd zi{}9i5wBQUw$30qQJZ=NWJG3?!Z%Z{204;~760LnZ;07JQY-f+S*qQ_U4suH_oAU3 zvvJ%LL3x0Un%1lYAurm<02I9ErIk9MQ6&+xtS=6pjx`hsuKv@9ft{%JxrrfGbq{QB zoZ4$7ct!|?>7L0@1OCZdS0msrUYKJp28ObLDLJ!qDvK?hIv58NJ#k3Ll*q-Fjj^_a zcEY3a`F;1jJU`VfZVDdQIeT(32aqwg3Z4l2QL{93BlFDP-E!R(tJ6;S*|Ue143*}x zz-$6BA|IU10urEAFl24RSTY1e%p8;r)8qSENbu<1CK)DU6o`l`vEBEx_mi^=?d*qN z`Gc&LY346Hb9Jd)u2gHK-hL5qgi7$e`wvh6o|_MA)~y9J0JhQb0qEdFmnkAPga+9n zTPR$&Ee!OAWpzRH0wrew;NC|bDh3G(paKEWzu)(KsT?6-sFn;D_yIyfDV{lF5RifL ziGu)yNRGJX+$FtIwI(6}fQmDXuJ5na+TC0TJb*6ufMpdM z6V0Z6?&v%t7fuA~kv(Hseb<3;&_du4c>w@CJlOz%-A5PNX*+->z2#z zc>S6!m;Uq7ad_j~?*8e%slWO^9}|k&nId3hr4%DZH;Fsn8H^1&g9>Ubb_K{dAUj+! zLuCT>0MN|fsa>gvKK|<0%Z!Ssy@%t!5VKqZGBYjRds;OsQ>hX z2Y-6!dtQ3rLz?CY=!g_Z3%()LP7qfHL>yM^1|sBRwSKT%M0BV@w;!I*ePK)ik4)iM zTfGymvxekUb~2SVa%Y)2~G4D{p2ri^jy2LDq8%HZ$ukX2Z z_+K7)_SnfqV~{|{=Q<@r0KkBmkTKjF6EapDNC=b{f}n`PrI7ramqk+0@0n|QkzEH2 z7?(JASP z${Q{_LlhcqKj;8JOE$Xl+IJnuCcks{J(3>1``%p#PrR@QhyMJJJ{QFU7hZU&u@1O= za-Jmr6^Cu*a(VO08cHC-JP$FmR}!!%n^I)IL}bblzozjty#Au$fu88{Gwbj9-Igu)O&70?CY%@xv|Lf|wD|Mr`OUH|cp4d<;?@s4EM zcwR8P##URIKN1L?6Js}3c&u&MvNA6Q94^zQ!Ga0c09oMZfp5LLXY;zh`j@A~D?6)5 z0w8ROdEIaln1QW$b&+}EhaV&)1SM8u@pvbY}ssH6319YJfBo0609;^o4+N2$p+hTVErj6@y7^*b*7y;U(4Y{PKQ8>NCKI z))>n~fQJ0CWgmKAZ0vo1^M#HNt)69B5COFa-~Q`=2>>c$=ry0vTQcWvSXOd$*(vKY zh3Ae;3kU2oJw72@no0C?O#U zpi_*eI-)A`9eDTK&YWC;IEn?6h16t0B=?OJB=^>!fc(v$+~`ZJ8H6Sf(Uwu$UonUP z0zQ|eMhl0nQ_TPb0GNsej1cTxmwL-g05R+R|M=TCeE1iK_e~uWRuGTC&Hx!_3^A^7 zq}EVmyK0PMHcU*0XtJw&CcUaI3X5^V=E^&Mu--3z|H|gL_z2(atW?;PQPp{mtqNl$a1vM&|weoNO^yybcd?hh)ty1f$wkq|mbx}FiJhXBd zs@je~0Z%PFmv({{z7>h_y}x~ZBh~&|vf6q;=#R)H$ z6hI_o!X!4I|KkrYrmAevSY=bM#>mK2Jcxh@kPH(51MwVKWs7KAftihB=|6w@fyIum zN4#d~l55W2Yz#4(#qQC^x8KJA$EVwtY31m!2b^t(?fXv{gkNsod*Ny8-g(t#LjWWt z1IQ#qt`PJ+Dh8xv*dP|*eobebY)UTJ?CN|@0OSk-;MkP!?Z0HrlG-5F2U}XEqNQP}HE+ z!a*}q+3If&7BLGVpl)0eDF8bT07B>@N7e$uiV7u0d&pn0d0_LFO?^G_TvtLS5;Wvt z!)S~k3yth6AHL$@|NHKf|9boB!wE@VjZKD_lBrk#P-HxuGO=zPikiY*WnlA>uin>Y zJJAtzmLhx2=|d928cl)hEtjpT)GApXnrY~G9qlb=MM_yp+_0pXr)qC60T8ieHi!n0 zEpsOi){r4&2GG0|86pItpo$0zaCEZUS8-Dd854ivi(g-uXl-3_`n%tJNdWr%pM73{ z#usx%3W&xS59EQ_m>`r32{4ek>9VcFgrJP-Rc2G!du;svr+0h7GgmEXPGsNy!CeRd z#?%arO-x3NUT`*(ZcuCRsh#_u*?!0%Fcv#2#Bm0A`{f%u#>9q;#$W=x=+vRS?X5AO z9yL^A3q*aTs4HY_Wy6YPbMwF1alDJ{*tk4Us~hSiTS{7_j!opEUNK9OsIK0a$`;V{ z>F2)-052U}TkGjDATy2N^W36Nb)=C7gV@bLfrJc_DVGgfYR-EQ6w#G6{>f+G4A$;D znFWOaVAypeIe*JwB{u!_QeQoK@s-wJe({G96HM45c7>CyC;c={96jZ*u0! z7*q8!t}R-}=-yodLnOhHhCzOa4lz}^$T^AC?) zdhSXhs+Z$R94#r60u9$J5iE-X0FBM3#!xx2NJJ(waU9iZy~F)ct^U}7Mx~roV!LWd z-;+BR2&o)9i`eYs`zG4nYnu8c1Mz%RN;B=YJSDM1v}7EyHRKS@Ohb?yvL)n!vTdgV zhzL4RjsfZR-?@F&NM*j|7krj0&9%es-N&~beeT%x& zHH?WV2(Wk@nr$P%Y%?{eh6=C%{QDF85b$%K`dbCWqAD-!n~%zIUW9MV##(dh+uygC zxq8nTs*6Ait6|O_QQX9+m#nAwSo9 zPem>!>#Q|o9L42}&)@Rc&Q3Gq**49#aBLBF?w_nA2G}4OWOSu^-#{(usbK26ZQt;? zd$PNITyr5rR4+(q6ILc-24-QCSVY6d#f-G2- zUwZpc%_Xt5hHgB4xa$Fo8||yETQ*E4CZrI8O30O7JkZ>+f2N(~tt?O;B4ZOk%QnWS zv}#HJ>Lmlt+5jlTQ!PL&@U1x4&K3C4NAC^*fV{l7CVe6iLzk!4oj8L^DUlEp$JGYSytys}3_ zjLaZN))_P=097K+g_4K}!f=%ZfdLoy9Glud=JVVK0l<^3AfTL>7fxgq!XWzB|MK|p zsd){BBbQKEd#FWRVx69CDN;9=JAZXF54w4z%uzI0OIjJ8oNDBmzwzTgCd3sz$w)0} zdK|4+hkBB^h1T)uh1`c$>hl1L8Mbmo>(C|4c7FrNxRM%YTzun|r@rpom4|0DwQ&lC zB zI!BlEIb=pMhQm;e+(9~)0f$UQ{A=8 z$_O~$CBh;IaHZo}e8tw`_x$rKeKi|9WU_L2*K=QLMP^@29GYNOdUbJ-Fo$7Y}$sfIjY=HbiVex;n?6{jZ%I@n&+ zc6bXL(Kor5bl(SIs>f zo$vNpDRGyj84m0O4FC(xP%V=d#+(QUP(T473#dq2R@GlV+7zI+ z*NfKlOtz|;3Uf8HCzPt0;pA7tXw zpvIvnTLw@c5JcJ@>Y?$iPoDGfe|m9zyn*brFiPrkByN)_b82JQILxoiv1+Dh+1N~M zoH%5=S9?x13mqSb>GUDqF_~Smw)FT>u|&`Q)78_HN3Q#`-7{?oApH?yF{)HSBmk*1 zxRCk^V>dPuMS~3O`Na@I$h^^j0M=mc-#EA>~Sh&OxqeO+NA@fS|tLog+euFtnGjOWgAQ(Kw{^Rc-zsokRx^_ z=Uy0V9hq(;8|$J}ZC5!bvzVnhfJg4+!J~cniNirP^E#U%0H3pBfRR>K9V2p~`4zaJ z)abbbQ$^Tgi(rWYAQG>?;!FSnz)Iwdu@`M#jcA$N^8U(m`xjkQO_KgRkoRr2CUSAd zTQ<>Lm%0HN5s(lmSBwmotPzSTN%@=K|Ki-_m|-yFHB12L{_WF}^LM{|+qxxvaa<`y zZsW>TgT1{YgT0&kYJc~+TYvS^u}5Cq=3I?T8EryQ<>|XWy0#Zi>Ghy$*bqSgL~DNk z=5+w5lC!}pw)D=ox_3P_zG@Kv=nt-2Xs07XQBD>#bkYn%dT_F76K71Q_x1vs`<`tm zMD6)pfzh~sc-JxwnlM7Z%!}r}40^-){qO$jfmW)8Spp!u;eviO(V3?#El12o!QgKu zK+)w|KHP~i&o08$s{&j*y8N|CjI@19$5Z&h?hJ{W8J+&V2jBL&gE_nD$PHEu5-1@s z2_O+H60hxbXN^SUNC|og*bophV5tb)g4eCfdz`^F6(VAlLhKgD-xDK3$b2vedDkBg z6cmpy24hUU;_3sn!DYR_-q!|WfjAgDGSqY0+R?Sk`_ElJx@x%R)iH0aBc#O~NBev3 zd}`mX4z*S;tNr0M!+WRNYlfrwg(e_@F-fJgXTrzMtmwCn$;5F(Ep-D60HO4hF)TfcbXkVTWki8am(A{hk= zN(zif%tnB^LLv;vmPi0+UYS#JYe%aWtf};^?j_{WzTwd_&o=WHj?KRQqH8BRIv>z6 zBeDPqum?8AAz8!rj^DZ|gBZ}-AR|4I0W@g30b-_S_MCJ!LXJ-?vP(wUL_LX*Bg4iT zh^_O6=F{yqEG|lr25iCqu3cL4?*&6=dhwpsd znIlhp_x+#!=uN|0FZ<$0U;pE~zq1gckNnS{AZTyJ)Lg?tqgL++rCWdRD+gvW510lR zs9R&*wP*G+$an931uiO_XD#WR{f-5P4yW-S6Lg=8oe#eGs(K?_4w#kbnQ0O{0-%v;h=c z*-a!-l-QMyR}59qGBOC^JOLoCWZ5^Dzv;piU?@=_%W-Uw2oOA?0*Hi14$O8s0dpPc zi;;kkSP?+y+Bp&+Drm^Ed^qhIBIh`cxKeSGQ=R#_44IR15|!eCf!f-YLq3Ga+2hR& zRAbh5nlG=p-II-N$EMo3N0eP-tzAdEJuyD8eJTy&LFaN=))yJa0`TZq`^NJ}*phQZ z&N@N>^{sYlP!t5z8*8RJ&PB136|w;$Bzt74U2(STEHhc6fs%1dj&Q?@s$oh~-}RCS zGuw~dd@&$0695EJ0n$RUs45q#rsvcp8JH(_FTQ-dHQSZ|aB^-+kzh8-yZVb~ezk72 z{~vz)8UUkUp1=D^AjcfBNzy<7_%A>C6%%b7sb|J0U~e^cj1mxqt8v`TZLKtnrVrH{ zg4qr+SwIH>L`uv`X7BMu9&eW-iy-su%o;>gM6{NTA&Zi*5`-0ljx7B0 z`H9CLd;W~;-vMA(U47Nxe&|y@17)_+7ytadjgHQzB*;cuhQf3^DLw#dC3|qH-Cv?d zUY_3Z^N(2ryPEvi^E)hbjkfN(?^m-6-S@xcQU&&%_Sb*94J9MUV$AYfx1P7@@&9x4 zEw^5rrO;}5W+TS7x z9Du0<&XPd@B<>hXaje8Z<{y8yVOk2vyZ)I&3m0B>`kLi~Z@GAx0(Mg$IexbfY+V7uH@XN)uX+A!*Qj**3%oum2#!THbUcCIWFV`ScIgJ6(Bxt7mWV?#>pgN@=!H*m!a z0QWOn1p>B-EffJ#qpcnZm=)=YvqmnsU|lOf+lzuGNd$nA<&xvF$*n8Tr*!fUI)uKCL+CH3yJU%H1&1BeDp3F00Rn3Yd>9XD=$+CbUyK?K`6Nf^%lAx`zjWLc8sHb8O zse?8{gVq>ljk9e}WKEZ%U5!#JB;^tTBiYh%9?AjVbzYRPT1#HaWmf|0&@xz4vJPzo zCPM3s@U2GoiY@g`ODh6kEk`l08l*UeRTWrh zbdBLkY(@vlwMv{gE=T5@_w9LA)(%$Py8igGwf(1z)ZcW*`b$>zJi2e{mGSwneCVn? z%Sw56Y`UFw^M(2J_Rl`>-0ovPdgySaY~#on(62vqxLPsaeR}cK<@Ly6;;347v}xsN z6P8KhK5&|;yy0Tc)gQ7AH`N@D-`?>^TFdVH#V z`DsfKO{QkulHOM55$FHw+Vjuti^`eD@BQ-8!WQ#wH=Z2;hpP?%5v_tHDgh`;N5i5w zbi);&F7>|yz+j<~swVD>q8rxrE>G;%74>SZ{@9Mir=B}X%$8~8P<3W8CzKb)^V~}( zYv$f}auL9pE{XzU9tnK^N1y)P&)oM%@A(MK?f=N{{?Sxs^!u(TLxDxsb6&3U?-2GC>rfAzxASJm#-cA;Kj?A^hOu1>N|6J z@2Y|F8LRtWtrUQsL0@&rM!=G>cHOEaWG#rcy$TpZrj}qPwksw{Ts!AQZw<3W z&V(XsmeeaNhwZk5t;iTfoKK+}sN|d>gGeelXs`$h^y*$0Qh|b%0VyCJoNg=7WCt5L zT0ETj<3~?U1ZB`L+wlZAu_y*VT-mzo(CWlZ`fN{MUwyF? zkf;q&hj_9>D~3yB(`hAwcCOZy5MgSu%P0UiROTG4wLSHy-O4nG5(a1aFaP-JUp=_z z`#*ZLnd!-9c(q_(`RUFBGud7DZh!j~Eqq=6aP8Y)MU z$@d;PmMhd8Dqx<4*>>(ka^}-V;^Xsq6r>Gs`R4V9PtIUaP(e@tMIuv<+o{Ygwhxal z%(m0_U$gv|FHcv?$wh02hLX}yVtXsNdS$=6W5ap&QM7Y=tn@UGK$ z?CJhu`}9jMb-w`-NE1D=S6Ju<6zL`tN4CG~-i%Nrf4s~s( zoiQm<5RKH<-+1Aw-D4BPnzwTS05(xX2SVKEXrRO?J46I88p4J?Ix*di4J_;DMng!u zsxnz!GTV(x##(3D2r*i#K-y6|-lk<`aL$-g^yn9_{LU|rG}9nK4$pWE%FGH3%uMKr zBcf-f6jc!jnL&XJ8X%tqHUvy;utIvya<^kDFI8Qhd&_1jLsCFcEB@rk8SsjL3W}bZGYWbL;Oov&$cR{nbMzt#{KsW8-@d9A8n3o_*nXH4U9^_|5)#6nW*~ zVw!eW58KhDePf;I*)bxFgHkR)M8!BU^Ie+>TkCR&mF&CU^1ki+c3gDEdGmAA)rec3 z02vWWjs&bT2_Tg@3|Db61w+UPB|~cmqxp8Y>C!DPADm1KB4Q_iCEj;p(E|jbRDuB1 z%7zh*HK=Sj27n`z4FEGh%)dmF2D)tPlAVW|uZ%T_O;BU4VG8woHRmpW53pfB}fg{)NLz781t|;x=J8@#Jn|lEjWk(z@=H{FY zJ-^v8*%MJGP}v&b=%rnAlZ)-!Zd~!`_9@9)C0RJWm=D%mJCmio$(t@7e&I;>pFVxf zFP}WtXm(B=Wij=3u8OpBu$)Qd>?J)5U117_;KD5{w=Bp1c;fh(t4lK(0qS@E^t`(t zoyc>j6HCwza$JrBl6sFmG@5zUC;%>|OelbO)+%nybRA(vfThyfUYKkvbFdMSNYyD@ z$JVUsvwwTnp<^lZT6``41XN@)49tLh-ZI`jBaTo5r~+9OgWExFEG|r z4|?{hWNa#3wxZtgSn9QJ+*;prxZ%@)h=}m$*WUKM`*(%hGZ*=gLK>uGpj@)IU%KWq zAA0>S9@=--dUy5}7tJ0zP;*dQwcaisTRIe$Ryk+TC9f+Tt&$&6g94p7}>YUWwYPDJtN(3Q<5R3pBjBPOXIQ<;J zHe>sb!`O@mV=!YAj7Sp7QcJCtI#=k*`Q?0bJ~{o@zM@p>U)8Jk?mhRM@B8-NYpuO@ z;H0?j6BFsx9YmVQ_DLm=kI--xduV_2`>P|zNusdp$z2C4RTK3yZNy4>w#gb=E_?Bh zKmI#^ar@+=`SmM39MJnVC#eQN&M$tqHdX@*9yT_0TnZt!B0!UrB7kAaZi4DE5e=33 zYXjO)CjbvumL!4GjTsn-N?A4R*f;^kU2Pj%V|$0p^~fYTKU;CxQ)b;3$s{?(7p|KC4+ z^0|{!|Mr_3IE@hf^{V#O3!4Ql9z_wPueP6z<-(+KjRgf+&Ha0O*v_cl_crz)cVL`qJYqu5@#6N z!W04)6c%W7Xj94*NA!4j#!E$HK91*cOhmXN`2$6uL2&gF7yh7a{I4A%nDO1A1bU1T zr|Eo7y4SF&e~a{39O&c4vW#zo8ut(|NWO0>dFa&s>Ls{+Jv?h&+v!o$NtB?IxZW>%{y{>?N6L>~y#SL& zb)qE7kt ze|X>XpZx;34q4L^L0CAJgO>ZuQugxNh>ODEz&F_F*{c8Y7CXDZz4h;2ocrkKL)(*7 zk?0pxZi4g)O>nr>J{grw@WTYhR+ad3` zf$h^nWYSrS+3z_>vhgF z;<25iZ%4hFFN;3^p+_EWj$BlG$WO)-ILjCC>-dBYd5`VW78=`&Q1+kG`E(2bWmS?o z{M`G`oWI}v?caap?#8#L%2j~D+*r_$Z<(}YHyXQ1rfJ`gRiN}J5NEB`{Sge@JMD%d zr<;udwx1*^nxY=G1}UT2U}P9tvp%(uk|+h2{ppuZT)SV#HvNSkf8>>ScJQSz#RV!x z^?6>N3*D_WCK$ffbfUmcLT`CeS#P+%_xUSf;38`iC8DE&nvED0izWzTY~~}?>SVFX z%gpk;aBFoRM?mPfNDcZu?_2NI7K&2ei~iuRZlFy2hQIZt6@1!$s^~)Wr~l<+-+A+X#It#w@0yw+v6=b&=~<=MbAcOB zHO;YBRlfCap8u0~_f|Gt@~kC=5s$kME&%Xm6qCW7F{!yxf;Ii0fBungU)#esotqR; zxvh;TsdYkXApF@!^AA<5Z{BWQ-UQYl*j@x&0{abBVi?gv9Fb0)64@vSk)VP&IzA!6 zLl-d4ft%hPCb#P_K}tJA@xhZ-RILmHSg(!$zI@FF!gHyGrJrsM;*OWXXW!89uS^$D zns{ffX9sRTy8l!2dD{(}qev035ZQnD#86Rzbw|LUhc z`rLyH>m7f$V;3#JLPAVAJ*)8Jt|B+7SYTG9b4%6bNpor{cjlSLcE0gvpM1!4az|T^ zz-n1WP)^OLJtx{}cIeC&wS?DlAS?iL6aGjp7t|k2^*9m?(j6EgwXYKkD@g#SsT)baUN7X#`%yG!M7jy-VAp^R4DK6GgqO%SWth z|K{I)?mOq#$=MGp#`o|ROgLo7Jr{T-@_!x&;a4y1b1^ni*e0TQ?E5Dv1dB#dK}9R0 zVoK~^zH%3(fC_cLte{X+DW3~k4VNLWQr42WCJ9vd6<7MnGs^klJ`kp(VA zt#~Mdt&&JG>BjJcG>r<50GmM&OS;r>$4^G&Ljr2KkDbUrd$zc`X*u0izLej*zkhGb zJGPM3W$Aj&MOlSF+8zeK_`=B_e{%Bk-(M%vB1q(v{Dl^BVh}JRCq(ea3Y^UfzxqV4 zvw0_Q-Ju;>1r_N)Fml`N)ENXFShDaI|Kg#;(I|N2PzC&t8$?GeZL`&{=ETJJz>>fl zDu&fC$u4%%|i&A_Ij& zQKAr!L~Z`dQ)+J%GgLDncjAaK7YK#-8KPwpa+9V%bnS#F+p(<|GVa5Zc@&*rc(eA# z_x@tqTOw8m2P~NMXTSVY z4_>&o2R1Q?JdKzi&XRap6}*s1=so#yum)i#UAeb2W3oev0aP27jBu@;$h04b<;fVL zJ;b>dpP6JEL-L$SNC%(#6u9VwL%!+-UaQwe`qb|99qPHGj2-yJ?X>}d-D69I_Q;+# z?XEjiD^@xjsI~ogQUTV*#QZm3zXN)UcBV`nxrk2B998yeeZ|OdO!(ph%33p)vij@4 z`Sf32tb2CAk=1L=iu#xt2wmH?#Id9_Bfs#TDlj6K6wP9c43G6vXZYX$-s4|;XBV(N zpO9+$g=740e4*y2%uG%k`1rYGGZv-_%BIcmV=D$pz}MJR6b!(`2peSfFXV6&Ql_9$ zly+s|n9dDi1{v`PQIMs)*iA_K(x7T5&1uLV#igkX==M0KxG_K%zYAoXXi(78#jNt5 zKDGSs-tJ!9v{zg0wi}c9Hj)K<+O_@qF5=kCGFm-k3k7RAdM zatJ+?6`Kw*c$!Fwogp!woW5^Z^+CO96~U5GD+FPGs@`i1_STxENrl|K{>JkU0w*yH z{^Tr#S%k};%EP9;zuxK2n%u|>R_d*(X|t3A)DGlX3{TPIc|{|vD7422SkfE1W6KQ+yE$DTr}pE|cbq2TghXJL_Ol;- z@aK6Y+VJLuD0cM{aIhL)cZ zOH+UIgA*5T?j#WbBXQ`5p?~$-Hd+6&wvaejuA1aIen?Vr>gXrJ?(lpLlFR{M6*d?} zN_?8kS-Cyjex-#Rf~q#9SUQihU?m9Rz(N^tF_w< z{>DcR-Pzhv<@n}K1k?>O$Zr6j=ntcQ`=M!J7Yj<$BuLJ%6Me*lkUBAAW_xS+o<)6o zlrEQ~u7_eKW&`J2*E_tTZ1^H%O1Qt5H^arU)DM_XfB4v&o1=QjOh|m9$f`+sQamnK zkx`73BY}AIB+VEKvbx!fY_t5Ssoc;{24j$n)beA%wWJC@_7^eW*fD;06!n+h|HMa5uq(~ztGD{9`~xit;oxVcwXfXhtTw&7`$ObQudEJH^8WT` zANje0ohLwn$Y0pz9^sjaiDas77hxZ=Y5alWiQceL_1o;+5XFc11sBAD3So11P{ zV)pMviVbg^fazcO_n%=-r+0tya4e@@fpvc2y;V&XQ7y5N&&DHQJ8mFQ{>P-p=PO9~ z=1Llv3sn*nS(0TTNNCR1&8>?_7N0*?1ZH`0J^jD__@-fIe*MRf|EFL5aAoGOVdZfu za3sYcK_z~rqM|AWOF*h(f;Z>K>Vi z7c^io!josr2-Hr1=NE%A#Sw1VUAx||WaMUo5>rDE7XdZs*#bWbiZ zyb`d|6u5)26k;YLtb(dy#sBh`D|>QwLQ@G67WfsQ6|!XEDJa5XL5xYg1Kd_gR0XQY zGfPumF8}1badQ}GgpGl|kZo$!l_XQ^<1tZDC00u+*;$7P8HJ4dMPqCLS>CjXS@^Aw zpT2VM*6*F~?fI;(NQ6L*ZC4RQ`$Gro8w{Kqk+1`czU{=H`mGBmi|Qj){dZp8P(N)AkOYw@tytB19Yzu5>575HaQ6<<7B?03oB1g;9;^;I!yH@&OKDjB3? zM6b;%!RH>6Uz$zWt*u{pJY&tCxVzsF#|>p+6e}`Nh!{;XIaNf-8>akJK@MU%Ek%MF z35rJe=niTA7Z#8vq|eUB7tWvGyM5_5f2{DW+C;n4CF>F5-f5jB?D+TCPQj2_f`A!n zXA{f?M$E8k(JIffYNiKr1v#$oIi8Q)K$A&^BynWuNtV}s?*mWF$C2M`_2}%Tc1aQY z(I=TxR$!yCWvT|`f6P8f2Z%MyxPlooAFW~Z;scpx*FWed9DN84;O|6MXglMIk|U!> z=oDpY!Hd9M(oRUZM~)O4Jq~pw(8k^<_{D$x;bo=&^*0Zi9z*?iS{XmfCms!|5F`L6 zau}roR#1$0_2|k7Q38lCTh1*`D{y=Pcyntg;DZhaG$ihm&(31_NZE`Ync?fLe&0=R zueVVZ$RdZGwc852i2{K1e%q%pSF#lNK%ofQjTur3_5wbyNzPb8kqtwtfkb|&NF|ZQ z4Ns{O;uRSSCt(%YA1AEyD!0*f{2;)A5h+rlWo3qtH6%nwpsJNPJkw((@#{Z(^tWC+ z;K`ScKa!Wa_;|8t81(2dso=ef+W&i@cC;vL+ey`s!juD_Wh$Z@(K|jopZ(Uok%7%V zHmg|32rnHk+&gfP#_8(xQZ^_`G~+OcXw00BKM#}79@}l+yqvQ}xy*);9o24i+P%29 zUE}#+b9(^VVQ9M_{STjCed8;dVJYC{8iWMgJVd!_R-@8~OtO$L@Lw9D#5Jwyx)Z=a$mOnP5wFh1hQDiO^rUM}l)OC3}I|xN`L{Ll(VaTDWVhTrn zB9z85sV=2K3}nC)Qvb$UyEAYiYPch4f9%uE>&3?cd*#HO(Q=F^B}Fckv*gPdu+)4t zrZUGC&ug_n5mEVY8AHcortV~)6ULGZ->Lj)~T9SIv zxZgaCxtP1N*?DGJUWW)Y@}E9pSV?rfn>HdY%d^FWd4D(rfJz=`Cmyi=>=V;}cysva zbEUVp`vzDk-7pk!A!i==(K|m<`rQ=|S=~~G590J-82$IR>bPf;K8zjc(375&k?nz5Ga0g&%yU0t#G_P zAkV#SaI7eG+yqhJCr>BOmJf8LCJA+~v4=7!9C`O|4MzJNo{4)M@9uVgF!WI_yPMxx zggb?rY5<}?{GNy3zECS>Br=nzVx`5?bJF^^FX(bQQBhTuc$c!t`=s0>K_=MT+(^rZ)$nezVDc) z-g^&)f8)c`-(MBvabz4}# zpLkRDzy*{&o-M^tmEib}lAB6>U@}9JT+7<@gwb(-pnM*~P&~Ctk$plO&RH>tE!$_F zdSL4Nx7!(+fA+ECTL-QzWoagzy9@rV1L}@UHLQR9xrbi6xRuYBkcb-~DU$TB4I*hB z^SO-T`QuqKB8qqgOMtvV1q#P`4Mzf1=Xoi1oYBnWRNvtyCXXSEsiHulX5v!mqQFa_ zY8v4*=H*{|pY@$$!=ZQy9YT~Z54SAs=*&~|qkcC~z1vD*O??;Ug$#r&D)XRjTM1qp0Qrs{9+sC?uw zN)YjMr!w+ae(CVHRvb4Zl;RWT^s9}eG-FJZ<*(jxB3)ut?wgnDVi?^Tq+i>0zxwIf zzxkzIRYZQoV-I}vc=0zLghik7_uuJ#cvdP%>8*A!B{Q`F2}h%tuFuj)KniBybVYu1 zFSyqZC4oE{42ho8RF>RFaI73HX+QtWkt>_M&%Lm`UF&bO1AHn)AVDa2n3BRGFauK4 zDX9(18UvLdu#FdwT93>R`nnpgeDIymf9xi?mem102hI?BY8A$uE5L zOW(efrsJ_w`~8AOqsX@QyNOi!rJsM{$DTX>@>`ojH&m%1k;vfhUD)Dz)3H%mBX1d# z51jnWgENg@K$#EEA)1NgRw=0}`AXz<#p88G&|D8;hljLO<*3nW=-_^{j*IT3%x*Ux zJ5VsD3V0+0IRFwA;7!8m?{>r}h%c{1qGbNc2MXUKC8qTC>yC*UH6|F1Q8V30V-#(5 zG;Z`F6(>JHQbmpI+?_Zna!wFPbI&&ZXYZol>ee zy=3u+7W9)jc{5;evOHO4|7hK9P_mz3^-K7aKQKF^TAHbvJVF7Q@p1PMs}&v)5QmkG zlqw={oX2KZE*syvUdJuEu-2Z?n5{m|s5GdRMOZE!9{D4MPq1Yy1dcSys^ntv(M5ZX z-*Q_ArqRk&g65rusPWz?!kcVdt0!!--}e>^f+2{DC(2onAqKm5tKF=-WPw#GPr^*^ zJ%@7Ny3_RC1gFYikXDLH!II`Cg^a>gmSrWu4F!E0wg&EzY0WagxeC>yt^jPL9FraO z!l^tp7@j#Qc6!Op&9)H?o_T8GtviEC(Zs&q+}L>RsA0)zH46|Sc=xSaj~VN~@+107 zk1Z{hlnaV`^=dbrCb0$rPNV#E1r}=mT$UiU$ z!=Imf>f`sg1twd`pLByBQs>2~%+krvZSLL$$bxM} zMh7%fR~A)W)vbl8Y};YztV6;EWR)clI7}i%6r5OqwtRoY5$+~xVS)gn4-J^>0$CDvdltZFuyWOHWiz030gw5Q`Z$PS~|w zzv5_H$kdd9xvwAiWwA1gOX*NSy7q4BeU%PPg#>LBj@r{c! zhLg3_uij|f*lm6JjmBploBAI&XFD$2>=O!r($h&0_sC`otDj`qo7ps zrp*Wk&e+lwrRCT!Kqhyo*iw+^aEW9xk~ILVh|^fpPtTRNhtbq*Wq)N`NRi>Gyi(oh zx*XHBw0LYzASZ%;w*yvznanX#jPplJRZB!xZV-kBpxcWg+0ya$JAGs_!jj6^tS|(g zl&CpHYz+gz-JMj@R3nj$RMm0BDxx$<1>z!vAMlbQBk{!VXgKNkrbnhK%Q`clsqa)O zD_MjS4nH7EjH2*CklxsjUcT#OHLhX`P^vF#OvS|VRhaRw92cyOlC`e)P=WuEqgvOF z?m?rw1xvx#`3Z|uS(909QsX8~kgzmX z3o(MaS}9OD1`Yw9jQcH3SQhK7Z)b{$;5V!+tK`_`y?x|1lkYuZXm0;TD@mdghc&Ap zgLrS%{dcamz}%tKRxS0|l36MNxw0lmSIMx)OE}rNyh0;G5;A0oP+e;p?vjycirtKW z3|40z?Y0xc480+uY|?22Z{GINCO(}fwRxD3^5o&Og$!=Hpqje%;a`5TczW913NGCD ztW*GSSuSLPb^`8hYd>6^Rm<~Yx8=s%Ezaz8{A3MmmnX<-CMjQ zlgy5AW*oYr>c;%obcpvNHe!V%v&Duusn1rO$tgJwc%XYccdDZ7b-jMKcmM91g2Ewm zzkgvpO@d-Z9w)*KyieRPB{38I6`O*T$waHQumr|#tN=Ga&B-bvcgFNiEX?*?b|R|< zfhS2KLrQSr$+O2-8>88x<%Ap=IIA*wd8h2rDHY<5j4J%WfBc8vdgDqg8zC!8)V;`p zhW*2TajF%wx7UIfPE}UA*^K55kPL}fz7h6Fh(I7IgP4_}1mKl8h(@SVd7zN;KmN$f zW*-1VNdEXy49|B-mZm3iu#tS>uy(%@qlAn=!@D%p+@Mw!B@*sgLb;&iOGdNpCQiuD zO-^Hj6(nIQqt9p{vm)?z+wJh}jRErKgD%%@`xQ$ypZ_<3b1Lb7*-!b{AE**rnz3|4 zjvd!x{d_^p6hWLMSxdUH9w{=iJK@pyEQkC3ZX>MK)Beb-=Cz55+)4{|pr$FN6ACxi zdy-VTy#oqH1YVjI`3DybPb+Wj4j!B_Uhhpi4k;8t{-byzDpW6gN6^=^l94V>2=&eUBZ>q*2fql9kq-foPEYj%sP1 z@P1sf-1Xxhe{u2Nwu^5FsTT&fRR#r$gleiR|J29My?%K$PEMz64~Q1h3;gS5+argI zl-wi1bh-%-?%rTo zp(wB?k1XEa_ad^$mwWe2UXG?rm zS0AqgPtV53C((Z+9LabSVmENZWvvm9hS8{Na3HhoQ>W$@r}Qj>$l9emzb+;R3Rf2e zguu|hwo$DUezD+Fk$xOcJF$iSP6z2>O1an+E3(SUe(dG%T(&gdU{ z=9zbQyS2K<{54(8QS&D!J$+Q)9i)CLzkX#C`%o4eF{hP{=bT_qHS|fHgOr>81MzLlnWddH>&~;iWb=beJz(>G?r8RgjRY zA(sm5U^Qa5Tk%FfvrPrCJY&VKm+~K7GFMz|kJOE6PW2Z*HT%^&{WlNr6Nyb7kGL3i zm6B8e`vF+lxtsv^u#p?QvujJJld)PjuAqoWatu+N&&VZBer7obUPzV2_e^j1Hdcl9cC}hAn$gIGj^RvGt*vZt++Nqv0kOZ|*d55J<2HNYz>$NY zkV`oPMLQlGc>^7jbOOJiPTX;wg;tUAmi0prTlbmT+d@pRHmd%KqK?8|M_xHuOdTxShAU-{*T-^4l zg)ihA9*MF2l>L=Q#8%9BWH6ILbC1Z28!~Wcs4@;WFt`g#hdy|Aey$K5o}T#Kn?Clx zJRI^UUulj_wA%H5?(xir9ys*sb`R;A51m=Q18IZU(%GD@&3yS&|`s{?>pY88=r}IEs}5$OAU7ia&lxOJaGe>ulLE2*t9< zKt1rpgnX=^pw!m{u3{(^UD7GtP@Y_HPUiMbX12laWQG0MV*sBx9-upkae9mevSFb~JD1}f zI|AXQ2#BHKu}8|fB6?2b`;l#@Dl(ra;peMUhb{A=DeK6Td3B?+GYIS`>0%>!G4h;2 zJ1As8u#;z21SH)LvZb-6(o|D61p|_q%>c`Cfuc@mx~2~X!9-E>eP2=Nr)n)6;S$nE zz*YFY-*n|PoXqlm-vbzMf4e@HP3pB4pRxLOtQl6MOmjM@8{afCZ#Rm5#2~?CX=Mdj zO*m2!w%hS;FA{0IIY=${3*WgKabh0z4zHg(THPB){=kd+_I6|R{A|40^jMmXMtM#j z@Ou(4sYm9VDf`O#OWv3^GwbbP6uu?LYaO z|Lse+$naTYV@VaPtgOlb8G#2@{;;C)tSrj9%JEWZy27hwF0cLf&rUzMoPXhg^4$T~ z4(Y_jeVeHs9YigValn}X6}@*!-Rgm1A&Ka}`+QGy=7$bnSRVpD z=S=;bbvF$L?>M}Mmmix~_WX#G__HOY78A#9$>bF%a-=y&{)41+(411!}K9baKZ?bZC0jU;Ki)=>yKG`13>b8%ZVC8&tzjSAoKPa1zTD;4WR|YeEO8} zzS91=ndpo@SekGdcbAby{_VD4nvtV+eZ4f9(SVJ$?lkK4;a)xQ24d=tn)P(l@!=&1 zF|F4R{edA2W@gjK>zn#OU>!M=@>!H@Ug`#&w(Iz5qLBGt-@n_nLq451k;0Np|EU*`rmaCahtrL2gyW{w zve;^dzE8&7iY71o4UQ$;nIjDI!1~G zJ!9s3BUcp1jw~XU2bXB&-}<*7eeme1-~Z;NN`-&%Sb4KcoDey2N%H^x)?Jp&@)c^Z z!h$qW+|pP(4eY-Z%ewx+i6;fYTV5>BlryIfWq#>NEwLHjW4?0Vnar#I$J0edt>A98 z{kAA#XVHYKg|d0O6Qo#H@ng8*3j(_D!{}UfLb@W8_?OS`kAA+Af z2=V^n0~HbIv%?FArZan_Y9?!^6zM7x9WRMpEES&{jiPSN4g2?ZUGm*w6Zytat=jOA*AwBufymAHNGct9Gr_}WEDX#0X4^OM*c$EQO z#rwn;v^UmXTeAS};mbZa*qC?L8(SAQFJ5O7Tj2eCCJF{^UiMm>yLPW04AFt$#DO~+ z_GQV_rosMn8Qvkc&pb`BX0-I)_v=`hQeFW`<#a-Om>izdhjrKOC%Xqe&mk%Z0hT|O z5VDet%3^6bKL1qJnl2PCZ1!eP6xP8yM1+jtM0?-iEPT4>ax#(=pqvq@u47SP_8UG) zTjB_}kM#qN4zP%PM!RV=^jjNE-sLruwF4j}Oh)H0NCIcB%750c4yRqMiuw&dH4g%Wqp2r0E?QoHE>&^kWbkV#sqG}^nY9ck(Gf44c zEGJz90JA)Iw;xI&hBxzcVd=E97Dt+L=>gEWyQnGReH=c52 zWi3M!4YBvHU$CA&H1S}2y=oSL{keVS;x6;4N2GJ6Xlecf)8X0>m{saV{PV|hyB$2E zM3?vxolPWatHYyhBRg%P=?GgAr+=DF;9ksB z6kd^}wYK-x_6SuWw#tkyOj%;i)UzsIEvO88p?+g~?OrR#fp5r|)oj-TWC1qNzVqfg z2RobJdwsXraqP~hY$l8ghrVv&bF1&(bKAYfRu5het=+w1+Dd#|81$3+zA(WsLl+5N z*sz_N)9nUEL5o5@acM&4$zulc^DUHuOgb@@k&sahJ&rtRLxE!>CRUb<_b%7?;>2Pf zhXK#k>OIL6T6XL}g%aWDj~-bz8?C4XNreqKGRA>DQB_=)vwLAi=Ox%!>LN4EBN=28 z9Z^$`_~M}j$6egpsWG-aQ$|t5Lg*Iq00hO5lb`tIPd4AZ-*;k*jVu~78s6QkMY1%R z;q^q?sf|>TzjVE|Jg?MN>QiSPzeXDpzo_SW>{JsyaeQ+7pc#e!*pgNw=@lQoYwP#+ z+jv5e>!rRM0fI>bF7Xisv3dT*&n&!t`GDrv3OrH;9aT!iBiyHBs!S{7kk5cP>pA^} z*{R#RE68*+ytvi^11w}Mg{-s9tgKs^tf~s^MmHu87E>t~GpA?NgcB!9)~C-IPe=z> zzIQD`av2`WXdKNOoMx7@3==Y~>zBgYZwZ6V(`SpVxcEb-3V(XJ69hi?FF@|l4}R(W zhtJ=yBgIz6V{JrohG37Cg`S_}X);Vo^7nuF$RD0>nkKo7TAGMNPM29k`7=4@z$bHu z$8yQ5Et;h8+~MpVN+p-j8&gPuk0_phv?}$&^wCNA@fm(?n2@i5hBPakG}3qXy>DNy zX?&vd>?llSfmAoY`-!7>4~F0iYY}NWB!Wp;olSsdp2{mtKXLuIsPNMU?6m`xti)Nz z4}I4ot$!3L9Cxy;{LtyFGjNb_&*>vhK8P+|y0Gb=SeB(ifH!frZXkha?bp8d?W>^q zcd>O*d1h6a@mr15I`l6teD4d1*A>UEZI*?5F4hr`e5RnVR#g{@P^jBvZanh$R(hRQ z094n?sir0ZCUCl5bwb`-qdlsrtl&<*l7z z&Gu~%r!0GPzS`*zGXj6^lwtSc^)}7bm?2gfb>nkaLm z^CbV#Gy*`5?M*V^B=C^bzILfj!^df1DFi)B8r2m7>2WMe3dp#g92SmDTUT2>foByR zT{{krWED?d)xY}hKl<&fjml(>m!u=Jx#bD7>yYspv_PD!J$9l_|gA; zwW%;M%7dIPb^8Ihl8~3y9mZ0GUdn9^$Q(B#a0tPY$T|sAl&QBIyE-Qecz-wI5Cp!t zHYzJ@(@Q2yQ6{L2*90~vk{(Qz$V5=UAIW<;&530AVTz50#mm z$>UVYFkr~{2U{&M>~^}Ne%rnJ`rY-7=HLIrcisAMp&}0*8YMp%j*xxbZ^oV%-MI3# zM%SGz%ArTy`XLRzVog=ZTa2P|B(+yj#cPb8+8vrCy1VD;iUKmX*NvJjF9G12ms)jC zR>fA^1qsn=I?Yx<4YzK%UAOHZqEr;m{%`*5d$;$3&3f+#ZGp$CYO6IukgYk1YRKEW zw#Tvf^eMC@-C!asBm2!|Wh-w80Jn;HX1c_vCZnp1W-vQ$6u9w~TZ(Awm_aK^Z)7RzVN-HeI_gCG7siPr_KH?FGpq-B3o9W7<0!%7g+-4iZ$8l;0^7A(1Qk7@0KA$aF%-)Y*7mfLR+ z_6|nCKR~j;(Dv?LtEeLT@W7&Q-UV(Moz3uDrrL0@3CXDi<%OTNd)yR zF(>|GbngrPFc^%2BxEvK5m68SYG(K>NOOh7PK(0`i+R63%76|r#EWY^TyWo`=6M=) zfs+%}@t|virmNzxX4vJWjN9`5!QVZ(b+h-OA1SNKu~=%{SwTIXI*4UCd2-%N+?1mM z6e_LzT!sl^M%Nk9V&XtZLSj^~M-iYM)*S}Thn@jTV0D13RvwJ$61lLOuU+5&wI7*!byM!!_9M$vx7PQ_=$GTiEoekhk)5Eb zrz{=kcIb?2Nap9BoVs*pgD%FL#IZe-i5=?Ci#P6W5ZkXv0(af9r7jykdfl1jfZ41x<(!OnQ?`W-73pGT{}zCEIv(U3`h((OOY5w`r)(1 z(FnlJ7~)h|JE1`d&2;Q!tm~r59rSGSvLS6oOl~Mubrdrybs2MDVX=Dni26`eHWhh- zW<>#yNz~y}RMl$#0g7*HX%b!!JIWdOg??CveB>sSKMzn&rT#CIO?8Knmp%m zddQb1nb8WK1><$zdi|i)b{4Go;`$Jk-gveh`MZ+wsE;~! z(Dz(BYC#(qlW`amh0}S-&;+dfPRNw!v_q(A9y@l52T36d6-_aic+*X$D#G(K>O=qZ z6EoAFyZFX0Ui#0gZx5MDoSvRA7pAl`C-Z}LoXw-ODKZEG8H=gpnS#a`8Uq$%?LNsq zY&UFohh4Jd$Dx9OE%}_}`<}>!BPR(7j3GajZj?@+0X^u9_PO1z1aW9}s`{a!D_U&F6kEaQ7IIArgrJblHYL-^6Bt%#@43oGh zS^6@Q9Gf-o)VY+RYm9wQ6Z=3V$mVz;1}!vJRAo&~c)`*|S)%Mh5IE42SO$}Mt(Z}M z@d+Kte6QKd#`{c#VeBBjb%Rejj2QcEA7Mj&>F@3Q(SLmRwcV8_J2Cfx{q1kFj2TF0 z-5`lP=D8W+wfgmQGfW^Z-|f|x^CM^hUwCnjnVm)u8#eWO1LdJdX1{ua-}U$_J6;e( zoz4(QUKoPS@URGF!vIFZaq(lb%B`B4v&cgLWo{TzQY!)s^kdNbzNnpB%x5fPhVTEy zJs$yKzvb$h{PxwBT+mUE20h2i7~}xVva+QxQJ@)oE~ljl`Ea2S&uSv$0#=Vdd076R z*KF!QN|{ndD;Y9S^e_mKi29!Noh>qhGl|F7EFHgSvbARHnY;o;+r52`W9Vtb^F) zJC1K=&Aq1g(6iQ;e*3j~^}}$K@p!Wbmbzzk6Ff6MJsOkaqeB^#tP~AkfO>olP7Tji6KTwg39LXTN!A zH4VLS_jxi~(hmj{)n$dIeS^~aLFNN+cI*}?;3ZHwWpxP>+>EK<$riKvRGxg_|Hbp{ z!}>MG-C=faDa95%y_tI#x9e(TB?zI6FgOI`Z=;_$7_?ybH( zJXK(-V(Ub8dRpD@rtLG8A(Q}jdb@eqFUN}r*<(X>d zk%#jiKA9QD>Tb)G$XF&hmKSkQshaXzpL_7qz1lF0vW8I2Y1p|L4QK@b!1&oC*=9d9 zb!E~Je{w;bQv;Nnrj#EMHz2Kb!}rXqfAaIo|MPM;2yqh#KNh$cDS@dlM<><)`GHDp z=(%nnCP7}p^9qH~yR_>XA{&OGq0pS;vc~`LgOzUIJ3V2%Z{B0vEvB=@)HfMHd}u*C zTV+m6!nCG+Cim{+pagY?iR#S84kG}@=`z8PiR@_5KfqW_$DWw70_1v}gp>F^4;{Vw z#Xqq)7AftsN6dRQcWGK(p3me90t9A(6F%{wa(^p2{$L@sS+yu4h=+D8%4~P1gHR)@ zG%QuMq<+Vv+l8tqj8->>dj|s=6X(ZA=k;3MX>@|V7dO2~B7ID<)Adz}nJh`09d~7C zI2;5wSNrT2|DO-t{+4~Gg*tAC;Q&cMw!hfCM9F(UU~6`Wjdgz=ENHU4pl|I4%Z9v= zQ9kle2?>Ou$x{oZ&X$*-m|`lAG2ApWpJBu%lfO?nsp~PI@_VkR^Ip&My~K6BY>}@6 zb9TFYO3khW<0w2fdbW(ro7-ygIUVa&-snZDtlT;1S4;+6`hK&O)#K%f(!bp~C;QIG z9x}X4^A5T3=u8vbzCHwdBdcU{1)4T+h1iX0YFr`+O2F_XDMFHf+}1RWh!<(F)VYN( zUD-m}fK)DP$VgdBhV&grin_vh2Qdblz&Y{C?N(25AN4zNiW*Ew`WhlRvonpMO1a8*)ao zRP?DZzz1*CA9=L=rZaM<7P7@p{JZepAN-wv^XkXmdu%czEI)eg%iS|?-QI6Bh5^}u zrYhxTTqTdnGd5-Z(K+q3vY&BwndNiCb_h1|(zf!@v^%sJ(?skaGE@@`ndvkC^eYg{ z(ujeng0Z&Wg}o*hMX3A$RT?U*86u--CIwKSe14V-=hS3_NG|f#F%g{ukH)rUjf^Up zs`S!Ks;6C*X6nBcerr`MF(L5z6PuEpG21 zD9_1*e$*bQr3}F5c`>XpiGOu<_4qR{p8wPTiNLFr%<^;w^{hMrYj~OI4>&fac6r1l zF=RSHBCr9F6E5x6M{eH+??eOt>NXgXT-E3`M()7JYUvDovq0x$DCxHUdEDAz|(A8|q^#|zhqpr7_VEFyrZ!qASZ2H1`S zIZ~QYuK@rXfQ<-%Oiet$h2W3_F?%=323Ka8VV_`2XY7O;N6s)*4c4~- z*}5pe@ML>~#I}7|j}?4r6piU6)O*U^gSdoj#2cJ`;fdx}9ca(V*+R`0hmP}ekL3Q_ zN;-*iS^|Ro5HIvNrRpKQ;gxnO8xpUHY!C%Rx^alu5ej5p(KJo*$28~o_?Kl((@hl| z(O>?9r<)*$(=;okQZW3@mzWuD>r7$ixPD7!)&_4}c>SHd?dAYPz;r3ZK#Tgm|GRJO zzJ7VIx@&)Tt!Ia+NZr77KiRc|o8S1J5VR2eaI;Gb z;>yYZnX;I!?|1xWaQNLjccuQ_yKAjx*Oevd#z8PEj84rcm-b18gtb%7h;tLx6Z85H zpHe<@j(g(Jow?$QGP>Hia7}%{l)JAoTN{(W!Yhx98?UhGfH63~+Z4D1Jd5Y2jblaQ zg+=kjrS9drf+d=`!?Mnyk zL{Ysr0;!2csHrkJh2(W(dK_jY&Q}ZArAgRp+50UcqdKr<2VGX-)zLSL9*TQr?LQ;lZ=)u9Mb%ew6ZS(zw~_A+(ipz%TBm&+#i** zT9zLy%vfvJZ)L2>?5U@J_0?fs5ZqKmn2)hbMVSnCMJb^I4!bJ`^oc+K$x}NZ&<5Xg z?t$Y-p0C{95=F6;(?BPQarEy$A`coJ%k&x9V|shcu*VE~QJq>SaMswYkQbc&-C;y? z#+kYktkmqhiYsJ*AlQ*!5{KZ;k53pD0QrOmfP1wSJv5gE%7zbZ$MPdp-q%D}dZ4&} z{A?N9;P%?^*o>7eJv8iLTCl`P3)4@X9@Ka4-`T2G)Q(a)*ljEolrtZCPqUVk=Fa&r z+p4cyQ}ax6pK)TQf142oul)9%xjC-ijPXIsvz6)Pm%ktm!f%TaqNXjvG3*O5A#cLoKT-ct+hJdDZ{M z3mRAGG23+}yNT+Dd3%kq;*bgNAbM*}+=OVR|?^i3N$ z8N1T=*pylpoFKZ^8azLlag*pX&rki*6EJ$3nepE#Ukra$%PeblftY=Hygo@6DQsR5uKnM1e|43=3L zjP)D>QhaV_byHAry67y=otqT@#uF~8LgxNetJx!$(Spc?gV<(PS6m@-mp)cvHk~~r zK{wq4SExuSh{Db&f^T*o+M7@d-4@Hcz$Sm?#X`H!tTz+9g9B9!YaDs{ z3AGM((q_Vtxv|e2R_>g?`}muf z8dxT(+!t^6Zoq7tmwx`~<^O%DK3CCJ4~Bu=uju^oDQ(i^I^4v^KKAS<{x|Ibkh?g( zP-m1Bwmu@yIppAE@J;e1Rvu?pSzRdT5-`Po`b1Lc++=JJ^4)%Sr~t6$fgj6Xxf1|G zJz7>ZT^!oMQ-{;bTj}+UVc`2?cG=(SxdjPKaEb(wkK>u5{=x&+%@tP>g_m!43|0KV zqZ7AA1{muli$zL}1#G*)Zm*Y@8BP>kFV;=%bUD7i+HVUQDhRZ%pLmZYr^b z#ZIUG@LZhbf=ngD#0)z8;+%E8=ADiiRYS5(e@vR=y#lFaYFwK-pr6D9YH zr));6F<5k4?=Vi&*+{gi#5`DG{AFe&FoMhU4W@B5-e_FA<^1kD&7XWQd+4y4#ok`` zT;DgcyaHBu0GdJC+4nFZ!Q}b8&eqzV#xai`(|dhqX{js;UJ!DDb_s)|b zp6dlTKEZkjjczzAp+sf2?Brx#otw|Rd$sGLKQ)RxE*-Z`@s6Kt)SL(AH7hS(xY$Gj z<0Gj;ev^2j8QR^B%$S@Qb1e1LkyEteSQcUd>ot>1NsW=O01xsK8OC69#$@2OG#w0u zA`zh4?%*=Gj~y!5uDsLtUtQ~h&#!etEa&NBR$>$A;nq4);@>}c!T#Nc=YHkJ+#rc8 zO==F~5n`1_9vdl9(m<3&U7#V-pL%NX<*R#ViriS#&7H2uM(=EalL$hV z`6r)x_%k1^paAR%@}2WdkZZs9{Vpr969zSIY^)E2l+{?Vjm#*Hx?X&?q@9^T#3wc7 z+RniBz0=dG%B71r7J(0}WG7+^nw%!#WJz`U_Cg*Rp_azD_~?;`h3&o6&5@d}zQuQY zHgYpA82J4G%Nh6Yvom@kGoGY!KlOLM$4YWu;eX+yON~a{eDb|N{MOSy_Hq9BjQ^(>6g6@jesDVyDi!VcVP@wGm6`V? zJ99^?dOu?R&eAnz?VC(#12#^&B>9z9%0C% zG#(5Y*3wg(g{D*rd{mW|j+GmQOib1Xtly=H?z+jyDmgb!KA_v1UTfoP z0g~iV3?j>fG^W&`)_{{G(n!R!o!~&5gD_a?WqH z53pq=9WW8l*quEG_+ocwSWtszy_+ThN<${GyBNV21JoYe>JE17;B-OxnWqa+oVDIw z2{)RqTFxmc*&Iib8fZCC`ujHf=uu;Ksh9fcZwYEku4cldCT0{=r|=ciOFyAM|bK-fFL2Z@>SnwBPZLFY|2V zHx9zAoH)Y-pat0LjLuY+3exe4St!rHIL%G{1f!f3cE4GCFV|k>2cBe39h05PpuNl2 zb~4k(*=o8Rw1RtYA$5M^R#06;NyX)mUgQL3ZFuwT=IC1A>J3SNvD1%HV-7q%_72So zODD5;?}e{k7#?)|tipMU+!%o23W0GsegEEiCoci!_AE=%fPxj22ai{ff>19y7f2=+ zx9yFc@=+oq@Pg4NcWf7&G;%@( zCPXRtryl0dN7upih?n(%Ds$A}{x=(>X-v}u~K)AFb* z!;3&0uh*m2C~OSEqRc@SU$;F}p!e&oTBCP%)++GA{B#a>ko!*3^U?>7&Y25yD|<9} zd#lzvHys}n>(9j3K9<}2%!B^%#@X##jPoJv>UTRq~XuhGx9`sW;F)__{*G}6*|H{f}aZ)jKe0JYV{a!%&9vW%k zMQP+ZjoWBlRkLzgOP>7HZ38fIR9^bg%b6zOeIGxBDxtWVg-UzgS<)j>f{Pq~7y8gUAa~ zm!%mMiYBYrco_va>fG7c)eR>ngws=2d186kZ%$5GX3-2{2I>hWB+F;BB9e(m919vR zD&&}ltf7tP?t_+opDbTX_#;7G3YHh{$18l=ZblU+ETXWuO?+%e6 zN)nlL%Myp~-u2DVuovnX-iH9hi3OflMFV~!20$$l@F1WX0WXnY%Y+F+k0J9oaFA<| zrE4Ku8JqW!6NVuZawOvy%a0l(V8G$fm&bz#mkepY=W%iJ_#t4QOi|}FRaltTTCE^s zh)|h>i`I1+e3V>T6xb2dahaJj!Tvgp^s}JP_5_hd{(wrNZ-?C;3dJ-yT3r12zwT~* zbN7yyfYc1*ow|!Er`GdCh4AmWGpAa++wh`6lJmZgK5VbICsn0l3ePMTL`C`Xm0i|{ zNq2Z>cVv_NiK`mi+q>ztR(#>o%G>w$&dtiB?&#~6TANLKcL2b+SPvM3XFy}0UC@`R zGEm`JgGH>p&>sY}&1E z40xJT)KiCxhLufikP2cqlHc9+Co9?4@3uJ!Di8Nh-mFdJ*u91~JuOe@Q980qvyx2n zbD6~l^pQt3Toh7o{z6sFnP2_A-|PI=<-NcCjCr`C4np;rhbvE>E7yD8x4*u1?JIu) z!oAjxJ8t~+l4M%a&=V_FW8}()kVR_30D|@IGrO-b7ccrPPblP-3H92yUm5MbB^z2@ zndSY!2}Ffvg+)kBBV&?FY^qIl055^fivGac>xUnCq*7Iwr%s3}cE<}xysiGV`?hS! zc8q;1jlKEEgI-farI5kOv&2M9{kgKTa(hz*csgaKj5sr$WdpMGQ8hs>NX#JM4(Vyj zc7|@E^9Q>&jUDjt)OWJeT&ELQCDG|ek(+qol;3YB9F_5&9cmc?v`W9>4xK1fgvL5K zbt1f^Zp6{Ruh@1Hfwm>HvqPTSjkdQ~%tjy%qo~$)c02yvmUr!7IP@aQc$w6qsxWOj z5fBQQBb&k5pZdh7Zr!wn6;po_#k0}2{XbYn{j*LS1pv~b7*9l%&B z@?`RQ$8IX+C0!SZUVi8&`x7bIj_29nb(9sIsI}hq4Ea@a91v0{#dYLzi~k z1Q_Bl;T3^1@UbQ3elyu^xKAH7?zh>sKD+G;x3)W|b3)&3crX`>9-3rNXZ-CU4>HT| zJD^(vhDMF-t_=`$rsl0eSq+^aUs97H_q%U4SSDHBb*qya(qTK4{QBU}U)h|s1O)vv z%i2RHj9DeE9ne!RAD!0os-gtj{znFc`QO&lQGcQ)e6SMxh+q}_w_2OXN zOJ^sLF0wtJyVH)V1t<%u1}a(P=*UH;k7ea5R=>|= zS*G6W9rWPvj+PQ+6qF&BTjoehi2( z03LY(l`+!(gkLO8HMUn2j0${NvWeze47snt}$^N8mFae3YJ>>2wr^q<-hE0 zIF(734P!;1l7Hcx;qLT%Htap3#UeSY;NI9WYk*JVsB$O?2zR{e`Oe-y}C8Ja*(h_W};kNZ`ixTB#hFh zj_PxYqj>ekR^#nAwi7l*iZyBRrkHwz5gVkk7yI@I)PyWi^VqSo;>+iqd(9{WcgUdv ziF+YANMsD@L(f-Mw}NWMh$RE*KElY&J^w;2W>jw5Rd7Us@R?A;{bv7#_f^b%yt5T7 zlvzCu_PV$?SSz6C#^~zp?HOq}Il&genD6#4ucg_k%&8d>jqXY@yD*zsm}cL4x7Hbe z>lQxyzMP`+-G0(Pa2+o_dc-m-5^BEt*N1u;62={G^@4JI+1c07OtROP6=jq=nq2#i zyFCPQBK0Ll2F zo|lIm&l?3U*z73fc?PsYl%>{#+h+5kND5}Jz;ouaF_<>r_mE1cDR=6{<<@#**5cP&KFUzk6mus} zz46^YVv}&WTAIr1&o7nMw(V;h+qd>RYxmdgg#CBBnPPPo0q*>cuWE9xfWRvr^l_|k z56$~2JB}^IOp~c0c zW&4p_XLlo<&qfJiLc;1|GBqJ4j-%6i51=n{G_{xRf{_{PC=VwmN-tFnCD!>0D z4^66Gs=c(kaeY!@KKfk6DCg#W>DQ8XzCNt`JvT*7)pvz~O9mTcL#%0u+m=rr9d3hF zJXm~C&CQ=+e0KWi`!=@r-2O19$t4X%dlcI^>libIVMx-v^QttkmD5G{a_0~JY6}nV z<3BpF{`&sg@3iBP>-T;9=$-wLCCr42K@bK0@^S|2_WSGcx#eui3%QEo4-zbZUdxxw z*|Ii@>Vr~=XOQ-XE(sZigExQkEg1x86H|zi+dC+-l8(>x65fk>---4H0Ej{#C8-nf zDJj|eJtX&{k;89d@Nwk#>%Q*-r6D|pV0I`-g#A%L72xJVuly?op7c|3gwXJCv|S~`hkJL z26E(#2*+|mX0y=-fiQcxT!0N$PIP$4CmW4!AHdU=oxX6QY6>cvlX=h0i%GlL;bGV4 zJJ4|)E%5cNd)M#O2X6Ps)4%rCjqfKknP1N8>Ym3xHJdktB%64FoSr?D>5)5XEE9Rp z)J8NmQIU9fS>{X`i_*|!C*lI1JGGRT^SRaiVBkexTN(c3gT>fSzj3?&?$!tiw#UiG zit@wrj;W3utYJ0;XBqM$YCNZF7tKUPD%6iMGm6KkhaWM`f+9suyPKYb+TnnI`pH8( z1EHD`8qMMPchApNWGLNpS*jd_>#vDHytxg+I{aSUgN5+KnPU?#{GwZ18*~Q(nLj@z z%{~-}hhKl~JZCI@bGhe4;ImL%&Mf4Dl-sM>M7g>lrwB+zO7oR$K|gb>e10=7;F^YEf0PJh zN*%$&Br61kC4p)Q&oVF|^r&lHrq#!Y$1MSV>4> znQ-1PQi(lev63txAt@>9adrSIa&20U94~}X49G-gsO72e#7fL$CBYCmSr<@VkDPD> z!d2jRdLF`9$xnLHB7d;*?a~2CS}uWSMGxsvs3cj zN5N55X&zle(1aiaG0c!Zv%rQamrw~maKl!YRBn+M4{VR0%BCuJur`$WxO1n$_WGOm z+uPf{-L8jCYCA4g!1cB0`d)Bt$19hV*H=bTDqi1X%Vq7triby)NXAAf67fU^Ny30n z;pEhVz@w6tkZWc{vLKwx8p{&M4sIPawC9~TSLxKf{?*f9LY*t%kkfN;YG@b?N&MRL)jQH6cHUq;pfbZ|r&hzu)=jFAY|cu-f9Eo54*4Jz*c2%=5?{pDNHI%% z{39ntw%2e5!|%LRmsP|WL8HDF@ot|T)*-DB&4N5HyVx7AUSm=aF?y!w@Hh5&C6n87 z;6PWsuYY$R!gJQS}tDR^+KM>Rmft&izdc3vFauLm^#GD!FPTt-gTI^upWnJAn4BbAY? z6Z2K@2G50YnyZV%GVg`*EFy>_+v{^w>yRcdSxayd|PUVq>inxHcEG-llPXr~430O%kv%``Kh z*0hWHxKOsRnzfk;U$nM|%-UAZESqRtJf2Os&M*_wmaGmmkd24luFoJ|f(k01IOPYxNd}!9pT8iDXL1A?o z!w?Tvpdm~pkfmQ41ib(7XrL;{T5Aua( z-}P9w)AI^t6Mz*qk7_BoIh09uH7ji$3^I9TP;(GwMt;Q4R;LLCWvCYpJH|tNNs*(c zB3Qd11?7evhWy9icsw}ptgfHzc@ZK(KTIC}v4>W#?*7bw`OLjHuds=z3a^U}gXVYkD+wUMYCe`^GkZgi>cJ@?TQE1M2@-Lfe@eNKOO)dtCRysDRSY8<4Q zyO*{PlKZwlt0cpgx7|&Ah9B55c4H=M?6l&`dqWx+@5hIWTDRFNYQ~XivuX+W+IGj@ zX$)3xt)yCEYqxgpnWdFF+CIuJU+&0OUd|TxlgxZ3({^Hz7$YAcnO*7NxKGjH%)8z9 z|M0}!s~ydf^F`GkL?SCrRZ4RcrJ|-CUCM*P##5it{6~+-^BG4gKbYrdNU%$4!;YKHB%=-hb3Z#@v;AC-i)2=QQ2OYobr`x-3&eE)` z1b`?}N)BWAUR=oMG$RMo(y|g(GE%a?#R|c>1;u8x`v>0lZuBHEJ#yG$A(hY>jT0~L zGmjpY?NklCK<6ESb?+T?c8Bh;)h$%5^@jC7zH+}8fPo}O5P8LZuvlx%GTrmolk0SK(U&Mz%6Q;SGT)6r`K$ zep!wzA;$eHNk}aj0r34={nmxuuUy{aQ=Ep8WlG<_)&pZOIazYuKv3lAtfos`$&!)P zFEr+FkWE{QZr%qOWB(9Cf`$+ zpdBy*$9(+x`K|2{QZj^_L#HaEp6`t!nJOv;lv z)=+7}Vi-gMpVWGQtKBzN-PLxw%`M~sj!Mtw3wu6)=*iz1FFJLbG?^eiv|s?H=nkUk*~yRp+)sVrM}JJ!)OX%pna(7SEVDDyd}H6e zwBiAd5M+ZvP=Ob5M$8-X?N;#ctm^swjjdggoPO@_G^b<$+S}8DY(-2$xb(skUoUn1H@x9x2sgy;%(Q4Ts zDjzxo(42-9{q{S(2N#NyGjm9VlW4@_w4>>z$(UK?zjHs>XxTWtL|JnpVP~)T$Rw-l ztdaG$caWDxuAllo))arIMRR*{CJP+>$gvYS**`p~Y$tJw&>RH-|2UN60544L@PZY37IetvSEW+em>xD6B!h%$S%flm}986XZv-Y8@efe?%?HB0kQ=O}_qqEU{UHDyhf zOogV!VA0&$k2V^Sz(P*qjDl!wln&%*Z73i$7neWx|NhTU{Ey%H(|T(c)c5+WOOES3 zJD1z;1O~Kpri5f^(qc1G>O_oYG3cLQW8O=-tMy=j%uo<38Dp|+!d}Lt(x0n4@$_WA z;qrze^fHTY-`TArzRQYr^s~9*p}T8$Nev{a0g4<`zkc;vlyJZBv!A+u`?51~@6?^y ztoq2aALhL}@AdE1ox4r%foZ9niG5DkZMt`MK?#6Ved9ZH_Bb5?7Ne9W9baH&Q9+23Vy|Twj1f2v$5$*M zAly`juV%UBDG`{S&P1H+n|h35Usl-uPD9Fyi?f=PCJ>k(pUnsy>K}=IY0_{AS(`HR z!AH~QnqS}7|d)Q^1fW-OKzJX%YaU|(k` z{MiB`NM^DhAnBJ9mO*_PC$tB!TkzkI2sYeI>b*kG!Q>211)t=4*ZRp`z*J1_N8S(DNq*_5DIzONz|e6MIc>8SUEb?q zLx2DBJv{G>mVID(`tr3)o4fb1EkE=l&s@8{oioYtx6$h^&F7M+9`unK_@^eb_u9cL z?_OKk>zZZC>ee%j)pkqB{r2Qx`K^t3tL5m~g2fE6X^L4bQ_3AKcg~dK0@&I4{2{ri zDWN*g`}oSlM)jP0_L< zmPZsiy<}J#ANY~o^`T0lF?vXSMnvuch-&Tk;wZ^aXMBe>O`zp4LooEb7RIdSMb<(N zsNiUAABC*P@CSGH+ONISZ`qv8b)cGa62Kc+U;-%!g!+Lij;Fm#_!7?{nQ=kcL!UOl zfy7C&I3C)Za;h}whlie99`(Fl+oK8G9Cho1$(3bR!JiWnQy4;;Gf_vUJFQWMXC6A5 zf928vujePm^VyNU>x1@C)$n$VKsIDFC;-&i)TD~S-&EN>8V$z%V0gkjI8iUzcjMZ~ z!;`(&1#=gkc+XO6e*o;TGOO=2QK53SD?W0x+U|_*-2BQxv$j?HyvDP-s%U(~+YIWN z5SJwi;5Jm0-$8(DzumA+f!pXMR{1GiVXSmi$r~Uq`<{PuJ6vx^nk0@Q2KD}>?Uv^= z0GdwB<~c@yW~O*%p*~0eJ{*~lAARw8^tQ;Bwcdm(k!iMv`};kAINJ4;W~cM~gGOzq zEeafN3VA$9;bM`?aKXFx26a0EEM}S+qr6Duf1&dFs~2y+bJbur0zZBC+5jXZ$u7#` zgHIY~KKx_$>h-8MoUAaD>ajT7`q~@od}we}xV+F*<42WuUV+3Gh z#i0|AcnhU)xl%ZE`oyt0MXQt!P3w|EuB)51;k?pQVr)ra&>94+5{4YB8%j1W>V-ry zrW6{gX;bS%kxRI}K69gnp5vKQ^U=hNe)Zk$M;@6ETU&zP8a8W&&Q2C$MQ@3z%|yfY zR=C|_#MsMd^1u#&LcH*Q{=J*8z6~Pko%IpqOnFnW3@PPAOX5HDpgLFJ4IaAuo!X9}drc=)x z?QYa1o`kv%Pz{AusygozYa1oJVM;|yOrluUHG2^B+O|2J2}c3RDOnn?fORbB5>9-q zdvL%J#|19YYHHr-t&Tc1Pbn$G#t=&s#Vn#znZ^c_EK}9Qf-Y!8G{ly`9G^39uJkW$ zclmsNjs^6dRZvACdCKV$8>bqLJ`$4gI5icu4eOhVj=z8}Qy31RB#5Z~PEzP$QtXy~ z0IHJdZubTU2fMzoUbn$S;1v#M*gyZ)SN`M={xBu4n!#`UpO3zK?V!lVhjPGSctkac zNgl3fRWUuapr(m{tPEG&==mCTy&wMAFP>Y!a;xQGb1=Ju;PiuMZ*3ghX}KGy;Bw_= zb8rxF!A`W?QnoyC>WH$t-W6h}5Z-8al1Nj- zfNUeYC_OS`DV#96f9+s5)RdUjlg-V&>v#6+2xLGG3}Jt5*YyF4Nkvn+yBBexZzsY_ zXR0UXfv>7}5x-K#(u`mHwf}na?Kcv!m~b-Vk$ny0c|4Oq4+ApZP-Mv}Wei#GKadhr2tY5%3T~ zzXMZd=8RbA_A6xr)Kpn!zzg9N4#0N=MD~HHjIhNMey{0v?O0V50O-B?FyKW4R1HKp zYh8all^&>F>~mjBq#{IomV-@enxg0w+)L}sAez)n1rTQM#_L4vcr*=0EX!mYZL zvYDxj*LL~Y={bYsZi2!x8D7AJESfYuT;XZ{2gf9NQPS8z1ukk?A4jdnp26-38(S9%Z;vfIvfEbAsyZ4nhwot5r6M1Smd&FSi_dB6- zKJVq`bNBD}13tq0mUSL4P|OJn`P$9%*Bee+4_TC0-GqPp?$!o0_+jj&{OVS#7E1_( z8n!gUm2_!u^+u48zxK&f72srp1~(D6pPf;kDaN0Cr0}8BMN_zN)<`a|cA7m$;i4mp zMpa5-qyr-oELq&0mK*LhM+;^3@zZ90vM`mc+CwG^MxuSUWFXGVJrKw#moXI_L%z?K zHSzXll9PgRp1Zp9p6(z#`$Ni^N5t1&sr5!-HV2q8T%67W@RnJwtT5-!O`I$k2Looc z$DvAo@aRm|%DnxZmyxDd3prkZB?Q(6zc3+0-5wV~hiF=w2*7_~G7G{VGoS_xT{B+r z!S_EuEeDfF4lOcXnLRy=_;Rhdrmnfsz$jH;#5`mRzJqJ#dUW*LE^D1 z`Ah-NjfOJFOhU7o46S0y3eRJu^7t0&=m6#{$OOr!BL9B z8b2^IUvL48uHW=~wyAQGh%%GBqdeP(W~=~OK92Eqd+2evDieHUq!}Xc1X!RoLm;JdqLDrD+&PK@!_xj5`FT zrRk21?c+%2fCzJQCh{rTA^04)MOKaXnV|3UM;D--nXJc3MNnhnh&XKmHK2$hcl-ewN&z`X1msXF_GKt~RM_d)jCX;`x8-}ShmGAuip&r~DuiQAMDXMk2}?D_+L!0DxI|6obPdo@ z6psQzhx=jRC*(oov#uAa6S;cLMcFP1C~6s1me?@X!~{LstRK@v9V9nEDmXrg;GRaX zgAc2R)(Eff*q+l55MbQ#*(y-Pp^QW2ar1bzF z5lQ4oT2`$p*jxA3Iw|%>9O1t>v-nDNmZgao9OA(4PG9C(RK}o)L7PbuAM4V7ADAce zGe7m{%WvQ5+CFuer3|9scxltPML$T21u+c>Bf=q&kvLQ0LVP4LpyVS81z|p?nhATt z;4%W}ESh-;Is;Vla$Zn~`{42@3!J2?#;9={gJ?%l)>`h?C>q9~Y49r>sG#C+yZF7#^K?5V#k+}(Wln|Iyn zqs{eitvBsSD+Si|_=o3T{qmjtjva=tXDH2g2$IUcNQ8}|cM02v(j zXkb({3=n>~Rxf(&vHa$0JD*i1Gt%BjL@!2BO!r&PJKwm6-LZSWy|*{&0BoKvf<3I{ zWF3P%?trk$fd8rHGqx?H_6X-lzA}B}^kUsX#FKMbJ+B-4y+}pQYAK4y&y>ZcGelTx z4j74LrlSrJi+xGVsge#i+a8z)pI#wjNWjrPzc0bERs z9o303t*e9un2FleBg{SJP$Ezaibq`s-`1>;L>UV*@<@mj0PX2@U53UY@*8z`67@dE z*ls*<6QJI_X_WAjMS=Qj3gjOPhA8QV(GZ7T%*>Q0ogo2JD~x+3J3G zFo?K}!Mlz(a%|WqZtXN-QUPo$p6oXRqbXPs3Cvc;dOkdpo0f;9yzy%FPa* z1Mr%A2P53`;V3;hZ*E{9sRqezzh*Os=cj!**sHbidLPEZ6*dRh&psf)MJKc!h^QRjRsvB7i+fzY|&y-vhu3q zC3v`= zf*c{GMldZa@)1OnZa8uPJfeV*kb$HL-Ww+ZPJTpy2#Yuc2E!thAS?`k4#WuxWsta? zzU#W=06p{Mq3!FtaY&*DD)Ur13c^{`nG0sW;b1cWtQy%slJNA!yLAUVz(iK!@$MN} zq!r09UAzxM1yUb2+3$2whyap8fCB!zoROW5OG-E%$ZtFfpWv7kgikL7xEu{ba&3)M zlGY9V7$;RuAg^NGnpIvdE|`KjJc#6r`wO3InvY zjC?-vRW@GV@29?fd%xMd-f*X@H}81O_6Q!4IEkes-Lp}ij(`z`Q3CiGz(P4smS_hp z_v{g|-*G!Gx4Pqf@VyHwt38wo8?`?0^1d6mo<#8UYvC@WFoyT@K!zI~}SS+HcQ z_8<}0`V`!G zdnDpfo4Ty&{t%SD<=70qPtkSTNo+r45y3?v6iA#?! zLYy#>)9p)F+=16>`B)ByDI?>+5=YLU<;3oK3t;=GVdF_7*TI?=1)5cAnj((TgaQDS z1$jeKXrcn(LhAJ87_!I1`8yZ}IYR<08~RcG#-5^zrYahmlu=PL^I(|(F4-LqlOXPAb*KNzCe{r)}3O~dx8%rP8c6AvW#3`jgl)nj^i^};y4NRb_Oj#)G@W;3N-ux zZ9tO0JmPr%u_e7f@N_LZO&A6 z4J>OM^8tc**bn;M;-sLsUCzh)#Wy)IuVrR`yt#fAZ$0QlVDD@@J@Np2J{p-P3+iDp zGbu1PS7bFZk5qcw_RIjZ6EdM z$V*g#({j4s@mPsR4vj{Iz3ri%H8ev;_M6LUvdHz;S~wO9PaX51ijjDPl3pL_B^Z1h zP$3G|pks@?2t^Q#mWBhI>%_7*n~pQ`_jgAgakLCjW3u{=lYr*KGKdUbK<`eZXo{yz z_8kDJDGc$rgJ6mnNk>hc=n7IKsu@}hr`B+?nzXXr-)^}LJ4hqu;#QY83lsNN_J8f) zeB+B>{(Y8bX|NNXn#g-`I-aS*H0q6fQBVAsQ$+IRQuuzH*o1i@o2Rtj?uNP|#Wcnx z22|{a(fp)%fDJba>)-ohc+Di|z<1QC(K4O+uAqX^i`rwY;>#ag5PwoOkVO%kF?o;zJtHGKz`lc8^A)y=v;==Kklg?G2G6_vdn4M(1t ztmrJ$w!z-&w&wvt8+VvWf>o4M1NX<{RUuOxMt;(`$m?M3vmTPk&{w1s_m__-0A^(# zMQRC&M3~}PbB5-015wr#P3}2d$H4(DO0kO`e2GnzXgEC+wQlYL-5lC9_`o`0O0u=L za$7fb-%Z;!2Vq=Q<$PL~rj*Y47Ai&m!n%-GneMQEW3T$&DcR!12Y>YLw=V*S+3)g2 z3xy&18}NaA8T)1cY?8=B4V)8gD?)mm@rljmmlFG@nmCiVTrPqI9t{4 zuE9tJ@FktneJ?EQBA+YHf^2*D^*;rii>(e(s*|z?uX!lW#~yJqPY;IBE97ceP`GYM zsQU;?)M|imQidCxPZs-lQ~cBECGFpR@r~PS9w9 z|y1|AiHk}i2zX$iM` zuU9DR6D8Rq+bvqeA{GrsT}NIfs{rk>@9goK;5h*bab(vTg8O(XK3G~Dr$R=J97@{- zvlROt0qZ=C7Bb}#LRlL(Xr>2@s;BxbwkV5?7$?I;^HO4u6M{9X`%3rOOlKl#DV^%cRAg1&3! zb${TXpz?6f0W{kY&q*;s`w_Vur_}Up;QV&{M%NP=)+GZK{9m_$bKv~jRON)T$hiubLdLFY_QdWBLYA1T+(WyETzKB7@ z8AaToig9XEnazr|J}1kB(sq8Bk_ii_b1BPq#VsIxQ2)8%wiot#C|N6Vi z1CJcsy#D3?y>jDbZ`$CX*F7?!8^{JSswj$CKH)|7?*0&wHwaLOry!Urle+7tP;ai* z?2he048xH#9}Ocyy_L6Qpp)3}S)}m*)=)0DnnB=pJEKDrW(RvgCX^AnH+B=jV?jt{NU854bMV}xFs%s^>p0Z&#A{^p(FOa+Bzl&H z9m>Zv3C9;3*0((_C(&?3l?4*lTp47KnEjhwX|6QFTu?Uc9k}>DQ4t7|r0JRQ5L8H< z5~3AGWT>o*yWhVNIP_=4PiNq>2^p;hwwE9w*FVg$hhR1=umLhW1T>W=5Gf22;D zz`k`mz-Mnd9*TXwP^kDpDhM#|b|*|(PH9Z1gTo=vfO`TU57h`*i^DTj zATXLGJp8^{WFMQC?%lo*eRO#S@p&P^LIh;H zwXt6iBhenjh@~SBM6Jw(Ss`AsP}VXKicU`oPT#@l$i}XwM-pdG_(hM~~6DKB+h*jstMhTm*>nkEShj?V-OF?TpytO(t0r@re*c?+L4 zz_Z{vL6atDICYX|27_?B7w!#0+g0-V01`!jjUo_O78I=q8=5X52kxf>0DRa0W=`|%n3DQLMjtDpYXhYoRS&ejIaL_AC5Qq?er-4$ z*#Ws_#Sz&jqWX$cLR1}8q=M9T!%i5JzfPQ}nt-3icK|O%<)MXkpi*|#uC`9EMTkr!ryta3%BKW8{hql%isOu^B1o*7apG21?5R@N>GO7 ztTdc|e0uM4Er-rp>YK*ukE@n4HJLXJ1?f#Y7Wel$s>s6?uVTrQ(5r=MAC6Zbn-Zo3 z4R$tv*l-S=EvIgDYqR&kpL+PpwIkT+nPKL+?Y1o%M00}Lbu#c6d4e(Xk|o1F+{EE@v4tPfu`-VnT2X| zqnXPY6VLp9ckM3$a+IbtEyL_y>(>WqB*1eaPz_mzw!Gt_mMG@Ac;o{wA430YN$riE zn#;tt8*)-(&jr>lV6D30!FC|&vfa1s`(IQ|uGw(ZrBW~qc>zE!s2gfC2opN>Xwak~ zf>3OcXGy}+pm~7TfC*PdIP@b0PHssn%x5 zPfj;P7T&W7ZI3fg_ z`h3t3Hs*99Wm;nS5Vg#KI65bG*cS(T}`NmeC@>da*C<8Jd4V<1ET84mGJFqnnKi- zffd>5X(NXcN|A!z(8aRL=gcH-160(i*?52h0B~k=7=iGfP(ML z3_3prs+^DZkaq#l@O++*$P@{NAw_4N$R-0M8{h7nF3wh|bvI6>)TrF-pv%Rtp&yyK?Gj&|XHUCai<2nU0tDq!{J zmX%^zu$#kh6t){~05Y{7Eq?CB&h?EjU_sv&D;isGjharU=`{yQp{N3NpvIb%R6Tyj zM+gdo6m=mb)?@!})TA-c-L?Z=?%|gnwl@w~(kCY@^%(M~w?R-KPonjn0y#$Xqk1OB zaS41x5EJUYUmC`NlJ7bf_40hfBOU9dHcRQp5hj!%vjr(=wp^m z7K9%?lA#VEl-?YNihtlRrXtbl6^1SJ4P}I~Ar>@p!q}<}P&%^A_(f!8VXAn$<{-RV zeA;yFJ~n&g;qC%o3kkakG&TifS5`P6)R@r;VikX&=}aZ<>RK0|nS+!7n+6QL8>2xn z5d>A@@C_w{F=giZY6nZj#d)ttxr{-~OPJ;)_UwZ7+#|)$fBqIeeJQJWoOO6Ehu&Gp z7$*${Fh|ic6w?ghyfTwjW!ddJAVDoTiVy?+6gu!qF(WQLxp?X+*|XiwzA3V^Ib=w2 z!^UwgZQkFj$nkeqdrLV3hf#On5ToMIa%hgnP-`qx)KipqyeXjcoS09cJz$eC6*)l> zF5LxbD(r2!-L{WTh7mH+nDi|k%|weq=<-o$SxnRco5jx^&+r&T+KVd>o8DR*tdkB` zP{V@0tv8|pBFiuRd(Hb!%7n6+Dn(vU1C)xdXfxMLln76p`h4~R#~^4VM-Nq>kxa{SfX z>mfO0#hQRhl#r}3f|*n@3MUsoGf!T1s!OU$?Yt;0_jZAkN;g+GK%35I#f}?iRcQzLfFv$HH1VaE z*IV0t1d5CeDr9 zSp!}SkG^>B-W%shzsQKO>mgo;-B#zIb7#MMzhNU{AlB*N z&UiqTRFMIi1@xM}hfZt%$TQ3SK@ZuZM9YuC+YlJT3z~vdm1v>WEEh2|7zF1(H?ev>^p##K@`%&I;7C@ zxu5w1-m(f2OKFeehfd781nqM%N!eH|T#gss_xw@~)&Qjb!1_sRK-vhlM`+?5Tq{l+ ze6iej!NwOqPW)K{Bj;d|8)Ao>_AaSTJ zjqmL4;wVcvtDf|NCFgrb(YHbBu|}m~jz@S*RP;%aKb)0xO+X0V zt-DwE2Cl$g*&KR4g{|AQanDqC<% zZnxXWn6T9iAw|9eaL3BXBfM9_P39!L<7VH>q-jy1K1PF&0~Xhls70;cACB;oX+p^f z!(;U#X}fX$3w*{ZP^_XM9xO)zkN%k_s*O8j>HD64`(88Qu-YgZri2Aip^ZWgcNA{ix~K+JWyT1O_}HW$B5^^o z6bGirp?=4^0|F}Q{P``r)(>$xuiR_uWBDscZ;=j-7#EvUq@EWh4z7*?bJNAWk62BB z z)!J^_wZRCGz)KHRxAq1(QzU=-vI)1>*1*rn!t4aNVdkM@d4pj9b_W-wKz1PsxKwt8Ws71B>l>lmA6|LqE;jb*DqoQkkf6;0 zh~NZmqO!m*X4QwEe3ENiIedia03~ZvQjJ^PS#7syQmtH=F|wAzC46^?E>m*gj9D=< zESS^mT(7G#3%V`!u-6h8^0bD5u1md^t17_pC^wJCthuH0xExjE0e8C4;n@)ugX_eg z-(;Mk!0v_-4R!4~jvE5l(P=CwCvr?5C~uf#BpiYf`ILkV3UNOiv!^NBN7^3K`bAhM zW>`Su!0f@ZV247E=K2v8WqwRUvci=tK z;((0A@+r0uMK{mGmXC_M;(9(}utM|3Ie4rP#97ov9q-Z4Jp1l2t0ANq=O&^n%KYk(6!TUA+NYnXwMc5bxxDf%~EXJx$LWcXHrjltxr3d)^0DqHc;eeqX5^Jm|4KYP2rsHP~zLJ+uPMNdQ znUbLxhJ;%L-pSA;dA72dW$4=wE6?INCLEH#lp2$OZ$ zWIdYi#sNk00HKG@vnRfcD4X(hG`J(f49|NT@dU zKokU>e%wbjs8HKNt?5o=fbu4?NNEn3DGm$02E_@|2l9;e0*U}Qpnov2JO>sjj&+`G z_kA4{CIo;g7kUgn``9d<#kC<^;PMds5Ucacg?@+J3vG%Q0a+DNtRp3* zY>G=rXxtBIrll&9ZLk}2K-G{GVCMj~OvihEq{2B6=}#& zw$H=q6UZ+ii-0nB_xM5y=Nd|Jl;b>4e(HT(982ImqKYhuZ6`vh^U9lVcrl&bv@3}0 zBhG?IT8EXWza!Pt^i)n1IU2jv@uG>WvQQK!^5+tJZ{^ZD@GG1Y2}fnXt#0sWK~k8s zsDiiBa#D(;;#w!LJcRr7&fC}a2B>m8nZu(;MJC-X?ghuzV-kriju}btVpgN3B{d6fc{r+8welpqOq#HMvzyM!e%KvFjn)9^CS*%M z$ppMCBt8<2*#hZAG>gd7L`|U5kj9UOIhD2jfbl&wBjl${J@D3RcF!UF0;TY&rw-q| zxswEeF3^w&_)8;&z%dAxk_TKio!#C*%R#D`s*tdFake|aX@^J!$YE|uDH~Xzcx|#b zloeX4__K)ZairweMqElLcCQ;BdjHb-Z|_316G&1F;=^X~y?$iHfsI&?#f@sTu`|S# z=L`|?!Sho|6sB`LAPN#kMFj6)NyH3^Qve)e3v^vdFR&E|NX3H$eKnRjlqQWh!luJZ zu?(4<Q0?6_gXlc~bEvn(QHIZ0GQAZJ)zP|{dp3ETCfP`8xW4FnEx zgUUFtTFoFXDo6snCLWcgprS=uHyn=##mo5-Rph*kEG{AB2kaZmjImi{bHoFbkkJsE z3>YbsL*=|U`LELm{PH#>F$}f9V>^^}3F{b#VlSbs!7>yz#$CeXRix8F%*yl^{4e?G zpMC1))y=7;tlx1B9(*}i%2R$TTt z?#rYh`4Ka1h|*F)97Q1Q5CMP!O=!Z2yn4DIJ}|ALHZy~Gs~_O*XP|HK5?tX^Wkte? z5+KN8OVZGQ(RizMY$T`&rkgj2Of%V}Y^WnfJUyF5X3Vq1 z*e8NiS*~<3Iv?#+o`WctDXItc(c#CIc2*BMI~||oN$CJDM&~$MgOeYZIOhdj`ptC6axLD3JfeQ;*H8 zZTBRG?!h~6tb$U&02P2312)CdRN?@XOfYbDniQVyI_cus$v57v;rwdHI7I?f%G6Bb z#s_xjGD6FZaUO~(v$-=olvPZD`^b!P$W*uO$l`@K6|<_krfIBdAApC91QvapHqc)O3LRB-& z*dT1~;kmHwZ8w9q8<$MkwLLhF4D2vxU@PPP#CPs=ce=^#ZM)X;ASsoG0m;|=7(ooB zArPgbSuv}zcbdNKleIOVL$n_Ad2LYd69PVROfGab4?u6{3PP=ps>1_AiF?i^QOCv^ z%vd6~e&8XLW6|{dAY<_ef8cWv+x-*~n?~BIGOu!pVvvUj5`7-X8WPlz?*mOmDyS<` z#T)>4a|13-CXQ#_y3=_1&r$jBjZ&Ond0h-+cA_AghJ?+aOAN2doyE*D0{>x-q9x`Ix_TK*U?;=d$B`9Z;M9-(>1+}%PC|*d%KTbtc zRU*6`#}A_fOBzI2`LTy#p|g^c**wo+&yD*}7>P>}#LvuF_Q>Z`3K&s@0S);M-Yr1f zqbLxQ1eALAKrZZV_p%Z-ue|iZ6D#-YJ_&@RJud zNV?w`O@RC+D>7*|tKje|W{oEokw`=}aHyiNIHC8PxZk#;Fql00NPBPFaf1m1$44k) zy@Z6xAe{x`iQA4W0DA!0T^975AFFI?78E2%K!wR~r{_0D91Yyb$^Iw-WR8U)0!z_R zPRI6u_7AV|!3^cr6Y4fisG7c@aeKt^4jyBYyU!sofjhKECe7*o6|$LMI^_YO1LAcBkDK zp>VVTYb2>DliXdw4Hu{w08vB4Rv2%+vmHg8F#oIm@V3s45N3N`YE{&J$IpPFSHywG z7IOeLQ!qfIAcp1-H&zq5wnN)O7PzszDIJd)N*WEMJB&np8N}ydGvZF>6+oP9iggmP zw2&Fpien`aOP=sOABCuxvLLz<>;opH|FJB8a(QNVduP(h3apPZ`NV_A_Yay#aNhH9 zwO%I~Vn(MqtBObz8+40EeJGw0m%#vzou9OZL+?NQ&gZ}Rh0h}toH$$l-uKp0mVP0l z{ozmk^S3U%31K(O5q8w=#eeJX{`B>OVXJTJJYSFnZ!}oa1mxUjKXmHywLM1Rj~%MK zd7~cpJ#4DuiyutfeLeM1JOX)OEZJfi5D^{VCjiqa+Ue}C4E<$OL>=4d;SRk!We872{t26!ck97d1%)aBNvJ&Ci z)~FYO{}+ZKTLx1=UORv*x&Y-fbC%O}LC;fjEf9F1{t~Yt^Ui6l1MkUS`)t0s)!7er zYaaUXDF`x57nq_SONKUTxJa4u8nyZ2Rly2N<5(cdRMi(BI~?!zw1QG2NACo|wzvNV zANNtF4m>Qxv~;+@bR0dS6z3ta4EZ1s@ZlM(JCDy@I+|HW!)nz8#jqV zwql1^4?s?nd?6c$K~)1$5?Uu``*oak$kEv@ifW15?2J%BCn(t|f#Ql(zEdm6;;by)+NnMN_%!s}H|{i9>Lcc)gbgXHMnXnXt8rZ`isR%AUqSzl z#HQc*%AX-3eCD(7`NEfPBG8Ny#-YNTxpeI{+Y81WrKE7hLVWka%|@$-zhm=&m%<{R zRJm!L^LJZ0RceC83_RZnuz!m(-)!ACp-}~L~-t%~YQy2)`LAG{K zYbW7mYb2!csR?5;ukMYKTHgoW)U`nsFnL`fc$tZIJCvg8nm%nBVS<~UScWK*w?$Oe zSWyrSSukZ@%BaNO#A#OLJb>6vcwuiyM2t(ihONHe8@atOsSlvuw+Cde%?WtkZExYx zX|rSFX(SahyVcXUeF#ySH5ypq?TNEsv`P*)Q}u7O@55I0hC2kxUNZvGe014Q9K|x zAfHJh%DIOqji{SAYP6|e3l~obpcfjzFiw|Dl|TH!ql(1ZnBj%re&Tuz9n9n30xtw! zNTcKE@66m&hxS*RgT60ugeYdIj}OcuwTnzzI?L;g;SjhR?C#JYZezu=D1RAzVEi|) zinwgkR0L8XF^V7p!D;s${44+1p}Ctk8=w8~!mt0q&wude-}}UI>GOYd?VvRRCl@3j zj8I6B=9!}gWh!*Av49DX9U@1;g-TkEU7Xbcw7+?A11KEUM})0TZN60OK6o_K==i8? zBvGVlngLwE`I4{?RY_jvVifa2%S+Ey6qVtQVHI{jtnlZuQbl2r41g5rF!HGilXiOF z+44Hbuch~ zJVHB^mHpN*juFiqJ;euPA@V(zQ_4kae~`!=8Q1LhZG61NyhNRGA!eO*V^nmkV965J zI-WG}7W^HHf=F&ZC9Sna?tI9&Ow`zMgj4}yAk@(FGm;RcmaKm4cYbf}<*(ugsR&1{ z&7`M4`PAL>_mJ9RL+~kubxKYu>fP<`v>|x4fjgGCAkA!|K49oR=M7u06k%h;}gy80Zwc(C(#5oiBJ5(i~8AL58AKU zD=jP$tS{VliHr)V0guwBAR-`%O4zuw*{#{(07~SDPEVxsD#}q+UU+C>Z@t4wYM91% z?$=s=fCPJMZEodon`$(e?sdWkP;; zg@WQzh0fr*5i4u$XgZHKMwJ%hVIwSB%L~Wp|1jSLH%Zvl(-CzIJ;riVa98eOW`@7Z-Z7=Q7 zkxj^nY-uA8;fwdFc|#~=fI)}6#BXMJ{vQ6-SI2!pyOEPd`*#Wt@fnrR^wgk7Qvkp~sR5?B&mQ9Ry#QK0u2L1_4 zj$Q?APw!cL_kW(3j4PM__?mvWP+uP){&+!BA9=Dyn5m_RSa?{rl<~aYjBljzioGb|shyp3g49LTtIIvWqN3_21Ztx;!y#vr%NBzO z!5^NOdEt|%?%Zm>{6~M_v_E|-_}~*S+`DzN)*ptO+wIT&?9-PnZzlxx5J-qUj$fcuifQ>1Zm;0J=6bA@*S? z?PSK2xgb{-A_juCNsZG*@*+bGgaBs&fR`gY6`lGn*^?#i#DX?=Bnw=nUF&~;$Jrc( zxBFo$LiH1)M8=2p8E(&~DKCwP8PYK3(CGxSC~WVy0jkeh`~V9aVAPHMyoPffKNwDz z)pa}S#?fwX1mqh55U390$Iz6=)r<s@To9t zah4d3;iQ?oh~gw5S_UgSK3WFzp8|s91VMm~z;nG(kTVfxxb*><-eac>hMwDVKYGr{ z$Y7x2t!s7EPj(PYW6`7Nq}n5aa#jGUlVNaw?13X98yUs>jcEPli(V_xED=D^c8w6E z_As5FF$qJ#C8I8uM0K*%>UqVi)Ct+K9S;+-96F5-4!|*T^+4zh;dB~j4F%`H;<@S3 z?jUV86UQA5T%<nrrgQ9I^vz5*2`0y^4JjoqsyAn&5q$idwoKHlHSfUN|OG-69DG0oQ z3zd1Kw_u9pIHk)>ndzg2e$yp}B1ut5WOSVSu}wIx8bto`Arqc98HpNp`om!DZttzN z;a1)4#wk!{n_+uF%;JqC4)LzTa+|~0;{?Pdkt`xyRJOqB59^F2Fe9QD0d*6!HH(S+ zHT!DA{@!XI4U-BMW|$UD31N@tnVcpfM=D!d7_o+;SfvWc^Xj?T-foW;4}fZ_82M4k z6FkXsgpKfWt&@P}LKMPmtXj=>0ti&x3DmssitKxUdv<|}5}8CjJ$aeMH(+C6doTe! z64@hF`O125vx&lw;CrBnv!-|zq-MfmXQp&}n57aMB056zemPqi zc*7?jp1i-+M4_IyL=Zqj7fgI&<2@2WKapSz{0O1W$U@>kG#T74@Cm^#;iyBZEKvx= zp@82ED2b`6Vd)A?recO3)zALaljpB&fK_hTC=k*L!U2;OBoQx-Jzl^~Vo4byvr(o9 z&XG}*B%`UG;{aDK94&!(Z+85xZgRI5;z+iZt^50q&kF@DEyLynoAcRnfDrY&rK=zaXc!7mDMipz*^T)WFBdJ z8UZg7Q9I7hS_;^lh{?+MPQsU$OMDcP)ft*FqMUlrCd>KRV;RSZGO`Sb)XF5N`H zLZTJ8abAz1!|iuHI-n&9m4?&x+iMMwMDr6yPEL4xBY>Gn* z2`wQZlVA8UDI}S+nMuB6!ptOJNCG5eAc27-5JCXcLor|*8#mdOElaYbE9v%f?&-aM z|7Yv!BCLDQ*=O(fUGFN-de&OyMJD7UASzi@#Yz~v{=N5ZKf8e3a}jVbR0i0mWlKw< zVk{Q_!q(;IzExi=L!$&qtI+&{=se+W)Vj=ge0sAEoDIh|7@1*|p+^_v(@|x4_Q?g- z4ay!IFnq0&7)UtMDxa_=+!ne2jO*^o#>ahc1qS<2{IrL@`a^~UQx%ESz4|o=H&;sb zvH5wb0de&4iGKV4dGn)BJ%4@?Yr{E|`dO@Yvnl_Vf9cf^KXLKyW4q38U|Yz^Nk)|} z5DvtbNolDcQ#}zsc1%S%ynxV(G|5VyA-vt|uASM=cjhLuXBL*>Ct{sa92LV%`S!GD z3u|{^pYQZz$IX78(Ab*_20U1~myKKV^V4R(83dHH51v0;y;K|2AOkmK;ka*V_NhF* z_Uf@psgw2`b7$M)mP!$RrTtM~qw+ZIpfHy4|LM1u}lcFv>vTkL-MUQJ&ZsJKc#uPAwZV!1M7&w0us)ol9GdSlopX32Y2h-8MB` zH0dh0xW3u~nFNtLoo}u+dT%{OyG~IM_z0G+wpF(c&Gw4Vr3Jcy-`7U~BiY*zo<6y{ zQR<SL`e-RU4Z%!_@d+ycQSa z;^YM`h*oaI{mrG4uUb(}qwDG{n2mZ5wDiK6JbXAJaX#IRTB#5+R5@2Ya8z1H_fZ{7 zg^KC;por7Sn92+KATe$!ntUxHGw`VGt<`E64?g+=weu&>pJc8UM|izy+<)(lk36&Tqo16gP5YDgnl^p`q4&;3u;q#0}%_u|*J6&Mj2H|=2s|U`hmCmz^Sfk@y~GLT=$S%` zLw~v5D=B3-NZKOaF^>>qnwvj5P_HHz(EF*xVj;e>7KvFO<9K!S|7b-7oG?Srw z(}ef+wPwn;=i|<`GwIZ^o#mw>#QLYca~?=*tI{Ej;yK>?{`o5l5Bv-04|p8gK&vF8 z=M-&C*-X)OH5728+@|5Km$KlTjZ=M&qQjSg7>lbB)v2H~a{)z9>IF-*1gVxvk;6Cb z+*&R}D5!Ti9X)nY3_6z?JRTd2RYyy^Ud=C z#8iMxwpqx4>!62G7m2_Lu%~?`mbBcUD{J++J9h4P%ct9`-&}cW9T5uSu~dpAtcf-h z8b#8k)>Y%;mI6++=X$w2d-MKF7YcPC8ZN?_AD9DEkck9_3Ir8%^;c^MuSyFL@y&+X z%;OUB(V)o~ihvxrnsiW&6@n&=Nyd%@%HW=k4boJ&RG} zghTja{#qjAfZ6k{i8B}~r641I;u;@3Te#7C0Iz=k5 zz-dPl*j8~_lD}MNPg73}ZZi~_fY20N5z7qvTN7;ob7WPKdDDys5NW+sHqMxVwR)Q+ z7TdjV6e3Rb3?0j3F(vg#Qin0*%2lDTW8r0h%Z-eyZrZW2Qj$F}O)zw@kg?ErUDj5- zX!<>k6ECB0Oe6zc&Qhu?3MWqtRUZCl?mPbCg)OCEIBsxOeODGlrNatd1XeAQ-Nt{W-;FTS|OzEi-E=W?F;)Kiz+H8lc2%h;kY8Mr0D*NdgbsZwX(p7>g^vsCWV z&GNpPVApsUFf;t`etGG^=l1RyBYiWU^j8}LlnU*?0<+(-*;u0n116diGdZ6jQS3Ux zXm;f zc&Z4XS0LpMQV4~$)?|DwMLc{K{%=TyP@z#?EH_Hcn1V zBVBuR;^LBmMSemT~&mmVSg z-tKbQ(d4Ax5H#2$=-J$8%*=+%8?9ADC$oFFxHPjf_Iz{n?0O5UU+5cT#9YJTya5vd zew}*uEUAepZ#30z03;&_p=Xv|eWq4wO@~a*bK&}9SNF{aQ*pQ18ZK7*2X2^SZp!#-3@WE+FitM{s2utvcI~WrOis*HF!aTh%;d`$%ZAB2aLyQD4XLb2?beSA&14w z1}YdTQn$wXqGv>8H1pL`e0X90hVjZ)J?Gi;lWtSY6dME2#k+t(0|`ibAkLp1`jQ@V zgyq?|1k~x&B>gTKG}a*or(8J+%%cS1d2Y+8N)U?NwEZ zG;!y_{XhH1^~b(mU$1PH@ChuL{&Y~M$r?aps|U^&O9!3phDuvp8(e$x+Fh0PvcdCs zHvKNTo(n*PY+#*!?h_9`^5uW*2%{VxIzC%(x9hcTdVZo>Y+~2j5knsPjGW}j35Tld zXh+}=F1Kx|;2S^_yFsWx7ablYB7VYzGd7Fu`Ku(!x`%F`yY@0 zVDD7_Lx1{zI<#+(LuwYin@1YY#il2H6?dEF(W4Vv>#fY*_g=MUYIWnqM=sXaOPY+W zPGdUy%I~!0I2yj6Tv}Ya{|$#8J-JM?xMLIIhnA6RuzZxGh})o*BE$yv=yDAr8C!1U z6${GuV>R&+H^{0?BroN=tIa|A;-j!1Xz{{wK9mddAH{r&xOsiCigrs}y@SL& zU{<|}{DJT(dVKhc|6uvtf44zqcY`)dZMB3es_vx)+}Evpv2d%|NzPo|snb72DP zedCR(g%`FFh%zsQ3vS`Z@MYe1+wR7;dPfnwcCnSEuJ5}T2hb9n-D{8MXk|j0>dB2x z15iKh_&%(a!`21hlQ zp%RJD83DVYL5N4<_AogGv9z1tsAKUvEYTL_S_nDQJE^%Af5uHZ{%|xq;kSG8E3o@G z*+eEbVJ-Itgd|cr-j?0UoLm&RBSFfI3@`cK&gFj`8+gMBm3?PXh zA2#;s)qA|zc-IU(=!PN>k}jQ008NbVOwom~K&!7~M=m^cp~a$kHp(`)XMFFQUfp=< zJif2vL7j*bmZknGEa!)7sb+=IC|Y-hGcz!!j?s=$Vv%tfq@B?>~m)7@@&ANcq0 z)ey3t!>a^xo5rRJN|iItPNa;UbznMCY^(OtMWP8rY4}JmAe4y++_baloT+$D)3;4Z z$j=t|JoOb26HWjy9C(7No-6@)*68^QE2xs4v+-&j+->-V*UT<1*WYvRJ4=;y==C?> zc-u3lSKfHj(Z#imVVf(dg=!Gk)pCo}2=%KJHzKiUR9g&9i;?-LJ$+H^l1y4*B1jt7 zndXhZ{<{5#|MPYC=GOo9(f;1|EME9N^aH@%M<4tW7bWUQCCAQQ+1)P114s&N3Zc?~X+td`U4SlqmWYiey+usd*l4hJ><1kRmbGg3WZ(%% z9$|i+DKwnOPFAIF!S?*4uYT?1x4uGu5!bRp;CvpzfV<&j;smKG<{C#qG)eY3KrAZW z#k1-3*@pb~8nk`Xs#;pA4oS=^LNFz*TV2|QQ3b{7vU$J?w1y`OOCMkOHq6s)g5i9c5%O`fc`ggWZJs2DH;I(cq6xP@3sHDm!t+)jm z>Dw||`C9&&GZ-{*o(_r-1|Y^w%}AtG+*DYp@t=(SP8Q36k_Mjy^bUD}^d$rY^ycjD zi6XfX$H9;N+{gcoo(Vtyi+3$OdujIQ4d*Uh%DH~kH&3lsE!#P@fC3w3GntHIC0tiy zw|xhL5qRG0fgEj%$n_xWAtX1AqZrWQmO*bInFu!wPL>D)OTJF?*52IiEbbQ0lxttn z2I2ups`u?aDF4$o9K5!sGm*4%;eE?k!gw|*leR}5A+pCWN zvXOlQ;EX9}Vc@Pf7O}5KFm(Nxk8fHQUwVM@b_j{Efl;2M(Ojw9Q@F?^Izu61v~BEO zKOcPinIhvHkB%q7>WRZ*rZW@wiKJOE;9ofT<4GH-N?J~JG?+`kedxkD+?Vk&vLeB2 z7Tt)_Y~LlwHp?B!DpW#xyJAe?S^{$4bpJa}f9H#!^%X+Bt`h2yXx{hXU;FNV`xu!G zPSHxEKQoztI8I@@Yofw!VQ8(vuYCCZfAya}PE`X(7xXg~7)Rdqy6^q<7peXu9z*(p zSq|=C#AERE>S(=Y@o|)Z%#N|wz-0nq>NPd2#kAo#)JKloI4^kJD{^>Isa&kKT%a+( zHI?kFlyXVxTp@#XM(loL&@>!Y!;J>LVINFUUZ7s9aTswW{oYmIkHaY?Z15fVMkdmQ z`e(zF(}{kuwOKk}g)i%HbXmFiiT8GnYwoT{YWafos8g8)H&Ss-A zkk%K2ly~rDn-TnI+Hy?+d3GVEpT-vP?ywfsq)`khxwr7!_h*(D8r5oJGMC8g zNGx6~nsPollhT9ek-($kGc%^bb~WP_Zzq%TLCkHL0MyLTlS$EsW8V<1Ii@Mx=i z2{VAW_jynMpUmWCsWOo7-Vq$t2$V}9BP z%cDX7v;j)YAT{SELd&-eJYsf(VB^#?&l(Zb4ohFSq-Ak-9{l>_X_=#*HzLjCP#Mf) zMj~PYJ-Za+R{P)i@)P)YzA2(LchW4k*IqbJk>^-;v}ZDo;-7{uK18L{Fqr03L7`*Z zaOXVaGxZV`gWW`_<#wezot$dX|V?k z76|ocuhg0(-)h{bJ+5Fh-%{?`Y;^S#G}Sg4ts zi5jxUv*QD82Jnnzgq>l7Idl5LH{f+5eQI#p+wZv^Dt|5^Rfm-D2MiJ}DFxKCooSy&Bf4cL*Z%lsh4}R;ZAAI_yGmrA=j-yYv255be>Qt3d z>Fyr)lf>MJoGA}}LUmQD5t%rUZEV#zb)drQ?H)%OfNmo2qDp9r7%j46aDE)Yxz^#G zOaM@)9ex`)V6YGFD#=X3T(fAkL|EXL2zZ zm$_^(o?tw#O9#@@r2)J9Vg?OXbT~av@Hll;*iJR0m?(YVDmYayu;5VK;7IuX)j>ps7Bx@ zh(sEA{xBPWhs?yS@c@6p46sD56$Bu#xO#x0YELOpZzMemFMyb9ql2Dz*~opjA2@xQ zd4fu>4n@<|r^Ajx5s)x}p!&Xl{l}9JdVFpnIEf!f_#C=&7Xw;S|%Oypul zt8G zyAjKS(=5D@tT{FG{7pEc2x=-B;3BS{T)$OEgu)NhN$U{091?P{Ullbiwc8$CIF zhj0AQhd%b9N5A%&gS$c|bCJ^!dMEC#CW-BvQ|IfSe)G}uXVx|t>tj1tUA?Q?XxbXv z4gUQ5|LDY%55Kek)r}{^r!z8{tkIcz=X+j>9>NG;eDK9H8w+&O!AxaiZeh6& zDS)gNX;g0nKd)Ej@#1#Mu+G(cdF-ZRRncTZ+|7h1f2P9YD2s)WGj>eGESS8q979jL ziG5dKq+WPJc-)vz7by?=GvIXGvFB4xCT+(?%4-^gFTD|X5jkDvXCthbthfN;zEEC8 zJ0cmkl%E~K!Ys9h>3zB7^*U9==*3hvoPXJl6W=?JD5I`wT_yj;g|U>Xzz1{vilCwPmDL-(}k#jF@!GWR4cA+^qQ|OY5uGQ5ap``wwj%pb_7hV8Q z84nQ|6&3WhtE6UAQ^{UubZ{2~P@9#t@}8eQI(GXmefZAuuITe$t zTM>={W^B7Ia3FqPV((6RMZAWBP@MIBdj2*e8wt;~~a%W-#|sCcyx zPQ}W^eZnM%EJEOW#Zm8uH{6rIaQV!+Ro~ZA{LXK^|N9R=WkyQfwq)mB8p21la<#Pv z32<~g4poP4zCLWzf$y3KF(=UXv%P+mm!hMI5{QxsD{2kQ^I$4Oh=rx{WTNGPM-*Dr zI!A3?5aM_$&XRKt{>WWnC|Z&!h>f;T;hV0ycC%b!eIRKlUk93QsVR7LTZ0`t!#Y!; zSn+oLM_+k3NX5@Bsjs2V1i6XzQXC}i>ala@H&M=)E^KOEnV}Q5x>-e0Bz;O|Sr-@U z`q&c>X@s(g?Gudx!*b`({L6`_KK^fCx^U>mJAUD3|72nF`TO4aPcJ_EISS5TxYC$1 z?W&lItSE4ALq0jE-KYe?M!l1aq^&GLg5er*`l^^)%1#|(r5zYzP@#3Wl9@a!?Q>|YTucgKt$KvAa^0Zmk_OLh#E#M14moUJ8R-X9w z*aWoRB2Hu~rW?p|X4i^q4b3OdE3@VU`l1byKby0f3CX~Kj*qKbYY#}OTI+y_=X{Z< zQMzR5r!y2fTAR2%FfNZwICHoVm4TWjqE$;#IV+Vk@Mt(&~FjU*+(o)l|9q01T9MV}milU@kmVTzI=fn7lKYD0i?Z zFS5~=Zcl?j~;m%-De{PTlIJ{M#tEg*n~DFlw2mFBA47ScrF6& zz{kgvq;c~>Fv0X%^}~wUNKld&FxSfhrgLE&Bl?B593}#fgzv)}=MW|W6dHy#H_H`f zsXpGWzxQ2t{KG##f;r&yIi^gPul+SJG9byCdPbxYlLy{YTG!q)m)wAM1inb5<@nWR zD~S6%Ft&xlzj1r8JaEt(5Y}SK5CQ&B-L+trMXytX8xM3#WU!Bn{VEtvMYnVQ@8>*eLl0gSSAke zEDH8xv_@lru2k(z&4!m3E9!`CB6Ch{Abu|S8&-zT4Pzc0QWlsD$wVTX3d89GW5Z?A zO6GXz3EWU+OgEDl5$R%jlUGl5FK?kDqv>fX<{D3XzNhiGJXnn(?1-{wzw!V5;1B-% zBWG9YUer-%3_-@>3EXmBZB#ewn!dk%H{UiLQ^do_!`N{!QtyWDmxsXNUUk{spvDksBSasdCRp{EasuYj^BFC4}WxO zdA%4d>mxd%Q)!}$nZMuO?UZ|!^Tyl$&N z4o8{QXHKn=1GxLON0*k1ia#*evS^5!?4xE#^0x?@`g>+FTqock3f}1s)j$s~u}(mw zAl;BXK^nS}JOCfwk8;KL6`9oYfBT=_ckRB|Gmo$EYdKxE(~AnLEXPkK@fOZ?1nJ<| zbXfZX$6kB+)#Wc8iq&R+C#P!^9~{_SVZBDND* z&rvD55R1LDA=wlneCjq)_O#YPeflwbVkXE;IGEVXY|6z41k-P@Hb_0WxO6&^a ziRP%odZ?+QWr~&)rMFB|uoWjGlsH%|asv?qEE6KqqlGnw11cxhAX z7W4eR0FSngkTUk{X~aw&fpRVvxk{tnXnTPlI^Jw5T(6<-J13U&+}2DyoB$f?sZlm~ z9Sdn8&G%h%>D(sIOHN{Iy~dBY9mI$Khd2;V%!N(`TL{~i5B#Oydi!@Cd}i_dvru-5 zJVVqXE%M6s43u`o9Uu@fGVD%6o{z$Sh!j1R^iG}KR7wwIrfXm>_{IJ`X%Tk`*&@rx z{p8ZvUlGw*orcmN!bTkJ798+)EOc);a4(aZnEkBQ5;_1#xy3@jtUR(K{?NZY`{bjm zG^y>kGz zt29b5(O3k#w)#!zoJcWSR#RnPD*Xl=viPI=6{J3D@9G$^7Xs9w?@VN4>#(jJhvmjZ zx~5#!Vf0=V3U#_jydzyel-f5zEd?7y0Ut{?l9~kfR0S$`(>dE|!N5QS144#MYeY3n zzABv^#!%Qcu^`ON)KynsUZbeh3XM%?k?~@r*}ch<@osM4AH!#-2R4<-j?JIV+{ctKNM5(z!J^8Xc2@ zOq%u}32ZT+3YSTol-Si%WAFdqFa6~46A9022n!u?SwT`x3|n4YZ;mAKs>c4?^?~GC z@z9~VwBAe$;5=JpK9bLUUWyRn}s4e z)3CTG5M}~@RtOcYrY0vabH7y9q&Fsp;N0lK{XwN?qJ}WsJrJ{KEGP{%O~*`fTw~G? zmfTgvX=G`DbyIuBkmLN>EoKrmODOjNn2aKHdU}f^daj= z)+!Q}3m4h{rd!reEn-oX>k!`#yk8_(eyfin$x402k(nmF4M)R<7;l=juHA*NLEGcI zZ+hm;(nb6{gJ{*I7zOpZbY>k?m{#>W)4~6J>alNr_A|a|m0K+ub%b7m_VnO0KYrvX zSLiUDr7lr@B$~!H32Ai;Li{Yd(!cfA>(@5&<`wZbt?|ig%jE?Y8Qu4$w_Lw^smR%Y z_WbU5y<+jDLYf#E-J=&e&`F@2aB=>bAeFmkdKZ>=Yi;q~n{Rn)c>|~WmACJD@ZrnD zh_=NS2MV~?b=d-zixXu7y|a+WOd@b)BIQ1?0b*nMoCK4ffTUpb1$v-%Uvc~H6OS#T z>7zuQO}rmzX#3qif7e&P_+&QgZ-diILiH%38M|tKz^cnhfvmCbmW7i)^gT0`hs=}epqd6S6r>f!Juc*Q&~cq0EM%}%<&6)s1E4EhJUn3}C>7cn(0 zA*zOWlrDS+aMM`Y?{i%^z_m_pB85#lx85dNh33ow&}0r%#9-EyH(qn&{L-maVwsF1 zO48J!JvEv^3r|#d0}{om?#{_TR~v*JQZ$|^$H~u{(5=F_D~^yM>=GKVjdim%h6lu? zUR1|jTdO+aDo#XZF*L%+KK6+pFZ|wJwk(=eWkfqsEvBrFsq_*PDV6YO7_M1wD#k-9 zh9*hh26x*R;hs5HDuIX7hKzDeQRU@-i`FlfsjnJ#D&WR;?mF?}X%15=7Q69=`BTqa z=8e1Y0&SV(R4#sUxkCD8W3$Tk-1)j27N1(GcG>qj{jqVX^I))Y1FhR$bQd)HS=7cZ43civxEd4L$mRKl3Tw*>v#I2^og_auGoiMTqfEs7tK zgbr_ac;{V*UwC5K&Zd$$9O+bMrBp><;RbB174a#tp32A=8bl0!%%!fda}^(tM(fL^ zso5+mKF}cNs-Z(Mxaboff2~))oWbi9 zFf9d-uhSP~fw3}+p&?+~P6k57B8^lGLCCW8< zNneCId*i!bhTy?~nR2~{9QQghaCj`4U70dIvMO)n8-p!dPQbgsQ86KoBR{v)?II9x zVD+>E6Scqs;!3-r!#$*1m8QEVn0>)tot{^*ZR;wkYVREo|~M^204 zLIaeAQGAJ*W|VbnjUD^r^+FS3o{D2S42cx`7c;(iag$RFZ0UN~fB7Jhu`?O)Q!G)U zK31Sh`1924(U_r4qZ)KX~2zI#sh6YKLTGlD<(Wv)M_%gbR$|7@PDfxVDA{iVY;tIOnLP{upQHN=mz{ z3d2sTZH-uq?hl{aNLWKSPGfP08h<1>>A7j%Y+4t;@g&hM{8^IeA=4Z*kV&_P^Q=V4 zQ1KF?3El_M!&XZIjZ=t*^hib4TC* z(M#X^Vy%Hog7nLP3cf^Q01Yv4^$t6aB4CI?50B7Pec8cZynpqnmy~C1(oA4Ptzu6l ztb)OXfO-zapRrwI4F3VW)fn1dZ|b&dOUvfOk&kwpjjXkh@YD*nG&G&~8wdT1Y<7z~ zAEcc-vt)^cJ*ItP*BiDsAHqAq`qqm* zKM~L?^5pZ2Q|SbvJ6c|54y<6qC_cRBwwubu$|yp1?TO6TDEf>_sh7*`7@B{QOs;gR z8=R^z6$o}#OK4!xAdCeua-iZ$v`tzpr7=%3Ih8d3GV9!6M`R-@LHm1n3Kja!jX%Mb{| zKRNn{BNo=spz~pjUc_+^O6HlRilNl9gOFh%GNCsW=arVm>!?^sm?Ou4S~ldSa(O+e zHrLYnb9)j-qZ7lJ)f(ezzqYU;M5Q&LY2o_g->kJL^M{FP5j%6}@A&9vUwq)pBym)K zslMS(Z0Gget#vuaBTLs zMrCELILWmtO~>N9Q*l0>FADg9ktSkJQDw{)9O>MR2cKpDV5i2Xhl!nyW`$#5+X8TI zy7S(ph4Z16P6qzfN9LPVITo$aaDLxg^OwHIYLAcSXh8`-jK9Uf>GkMFY$Ox-GqKPo zkqdBqc5w-tz@Y-EvZvEj&!FYatM=z;HTCAV|IMY-9~pES#2&x$-(Nca%*UByLX5}; ziTDty7``0CyywpO&pq_yCqMEL=n4P#pS~T%E|R8NBJK$1 z4883JHf<}_W<7AOI+$s!H!xmOm&9Zdz|UP}z~^Ztl9;6R9%m4{e8(&IcP?g9ytB{M#@)zKdU3oNt7406Dfy=ZDhM>FX=1FG^ZGq*^ylNcC;c<;Lc^KXu}J~ zO9XH742S|2(I*cEgaPmbyNkP<12%OG2ss&F^%gJ_64cFj*5uAyuTq-~5uG6`L)hXD zm~q|Un=}TtjZ4M)TR(8m#pf=gh7jrw z{FFu;R!!iDA(Bn_Nz24j*^^N*I3w#fm)7#tCZ&Rhj?QgwuG<~hh&fykbbfJdY1EVa z;ct{Qa?V0Sv-gK?cz0>-2dHK8E7J^eCI47q-o17Yt^wlcL;j7;(^aU&{tt|Zd~V_$ z+1|(>SR!52aZ~!e^QS-2?UeV<=1R5ZQxE-Z9lLK?3P$CEDJUZe^Ot}98@1K*3#*Nf z{?8A?G1lU0E}W=D$gA(G`2W@$u72svvLk8|6IO)d?%F$x-BEJewD!#;TI#nfa&|KA z6exwTb+fKyYs9WZv4yy@bGuXBhRUlY=flDCCs&(ge>5tEDdd> zVky^#&K(4f(q*u3@XF+xU=tx~NvbUc;}DP)D!UtuS{{P3erIiKn=rDd)Kajwv3A8z?}$$g}n;>l^YkuGfcs+M`f~D zx>ldVg$y9L+#0KdEoBOWmPGy`C%k=;B@q3NQcUhoCf10YLySgu_p1*#mJ89=4RmQ_ zpeyHz9k}$jqhecD}ST(f1+GUR)%8Y>tK$NuMn>cO-p6mCah4>ieZ)eWlSk zyB*Nfcnz$%%fHBTT99p_Ryt$JnWM9F<45kjaPk3ezrih4TRUf_ z3dM>=s_yu1TDR{#^2_wUY&W*p58av2QpJ~&kBMyhz>!JD@OK2;4j_=geW!C7D?*`z z=}K*r9g5&sYwwN>385c+?@3tdkSj=wql$Q+qtI4#eCnTxytt3$jep zqp2}$5b;Aqf1o<_>A*dfy6K8>%7BBdWn`iyS^XDf>9EGn1WCSul*u z9}g|ADVrK~DFhAU+vH2PQJi);w@9TjPhyL4s{db>!_#r%ofex>+c=ALkNMA`DefR9*7yjX+Nb_*<@D7e2(M~ZW+vhlO66>cSarRT$a*nLJ>K^2 z%hn6+l<)S1VxUa>b-AHp&P8RlGVw&L$-pHA-PPm zk2;#nZr$~3uU&ika&rXm4DCfj#9-|0+M5Mds5aqBN0Q{$D=q8N`J$@CeNDPR$z6$d za5MHE%oU5MX3@DYo;otNQVl5ag$gPhWj|WYch)Xf*>j$PNpwWFa7>gvPsGkYvbc5W ze0hl`NRnAl3saI|mA3MG?mW7=ux)9PP3%#~GTU9=yxmsCAkgwTOR7OAE@ZNZwI_ zfRIm(hdGZHdVxRceCz3je5+e(x7JtIpspB#Se36FgP>xG(I$sfgXEOFV*|Pdb#s1x zz1BH-o+ck8g=i>q;`!&d%QX?eEVYQ;Y-|OoNrWm$1)urY-}DAj%&>_nQSzv^@nc3F zUw7y4Sz9l1l4+jM$6t2h*WdM)uYLXVwjICgWyiKR^Y8tI`&;EbvtF@Sedx?3TBY@< zNCeUS%-?+y*MjSm%XRsJeD9r_6^O3`XF3|7X9ETKQai@(nfObqjcX1~EM6d$00fSG zLBK>V$7YF;A92|S;$x`9Sb0)Tf2Viw;LOEKc|?=TkZoy{+kOz^d|y#N&@8|B{qO&; zUwiA*Co2ixBvub5Ix_$qPlj^Q_C0Sr<4`s);)ixnfDVcl#fD8lofdkmANy&jz*OVDO;m3f7yDd;2m%{J z1lB>+M*U$X?oK2fHqI4)%MCqT)t8-n=BZ7?Zfpbg0cu+r-$+yS>nZ90uf&H#eZUNd ze_PsW^5J03Xbm<8nlFfrWvH`FOYWIzOJ5A9cJC`)K5I+2C?}&Fh{MTO`Jy1dp18e= z$2vVn7==<0V`IA-jTLS{b(BN1R3j+T$}5ylc|M)w7RyWJUPmRugz_RVY&SHG9n*?V zxlnH3x(6L)wA>;_Nz(ioNZPG15 z`=epf;2hFH7L+hKd*xPFy0YD>7YfOpdqk=cOC`|AFVot6)t>E(`FK1&J9mq1C0@Mv z*cP?cxoiIWhd=nIAN**$eYw$H`}dFi?$x(khY~-2=(c93p_-1UI~j&E2~^-3Kl_F^ zfBGw5K;3-d)8DXjNq&f>cBD*2x=?`2KMoxlJ9D9kGA2h3m9ShYK{@2v45gHfGzVH0 z`%wv+Et4@j?vrsxRB-I@<7%yr9nq@Le7fjJuqKW#iW&X>rO$jeztx&hnz|o%L`RO& z&z57zWD{|sh!j_uLt;usaAPQts?Y&OK$R%hR$-2vFN-mehG=I;RR=Pf4UzsNe{3^A zS;Gt%lTrgeH~S5R@C3@^+BlB}~l$2gXKIA7#6tv5MXQl&}iY-!gY)>{f+o+cDa|m~#OQ@f5 z+$*%A*1ugn5jyVh>#r}K-(bU2o+qES-QSV&CVi)4itLGxxY6vULLc-MGYi|fQyuOZ z52#R8N>u$zqIzgOxmBvS-u>=dPCmcVQ59vMzwdqHTON7(gsOlH$%x~6K_-L2*?(xJ zLv9Qs5S>qeTjDEtiRH@XW@YVN|MlzLd}?ynt^eu$ne(R)^!wv~^x+R4zaG{S0Y>}V zANi*;;GCR(xu9}*6#e(N78h>ak-2s@^Px|D!puabZ4wqz;` zc{O`Rv}d!v*E6oaYOdAAko5LVCz#8Mwg9`r*S_<0M>?BjWhGD$wnI{%CO~XqHf_HA zj{S=ZdC9A#`k=V4q)E(?7J1w24z1FzsE$J+(pzAQcAA_Gs%6artTZ}q#0Q$33F7p& z9$42NPWsJW+BJ3sf_<ow$5K2w&Lq^GK^+vl}!*Zzx7OK|P zjvxag*r;#6fTNR4OeFj|u^XQwa~i zgoDa+0nhV=addEVMHLa_TmwDHgA&E!$lZVZ>{I0XdXqDn?bGGr(WF}(jKDRMzV^oo zXxA<^4Y^`uYr<(Kif*q=Z5F6w+C6DrN9ILEm_w|Adaz)?l$nsfqnMpL5vr+~Oa#jO zPG=s)pHx{ zEUO8=h(J$FC2{Ny%*AuS1~GeAGB}z|zHBPPM1ft&@bFmTj)RupqdkWQMG~n#GA1^{ zG}u3B>`d&rCl@)w`_lsSKo;$;>d$@3@I7dYpxT*ULvx*_jCCIbPPbIFqe#fbo zw#k6AJDNT)vd%yIe0Ik@pZdf+diU(@5^+pqpHMfo?sV(;=w z!8UYu4vwXEr2M4nDWSa>hhKS>zf}YANcA)+?$Oid77=l>ZkXd~GrDnn<*tSL)s>=Wr4SG~Ebk$75CXzM`ex=)S5x7aE8vE3FaR6ji zTsykB1~Zth!i;FX^svWC5xk9HcV%oep-`qrNz0V)hoV;OAdAXpY zLV1vHVV2?Xr}P-uUD05P_CkW`q^sPBtGXKWHbd*_u6Ly}mOzuiO0)a2ZigJ1c&hcf z#>%gh+Dt{j4)=S!Ui}wRu65N|Fcx#x%0pDcOz7S?nM{zN0^=7tXR`-pM^#N(l8>HK zC1Yc8gcQAJeV=tqIut)RX>}SmV*yRtTkO|jq27;&mF=eGjy2m&UKY6ikQ&5^o8I>3 zaU=Tr`~&5+%O?_@%bB#bxUdBLe(_>HQ|6)LTtPv(pM-d@`h5l|`3X?+xSK&fMYgt@XM}EF;a;lb(}tRMH{>c&HtU zg$Uv>FpY622EwYH3LQH7_#r)*w1@!?PpQ+7Orlgix6u-bK#i9R-M+X$#%Q_GPGv$q zn|4-pG@2#~I=(GOg6@+_5k!p_AlGZv2o@o)r(MrBq9~5e$7;uU>08MOG1oEP8mERSQU3~7q z12~gn)+nK7AA#ecY!%vy%NcX9NsjM`OTwwbmzRkd2_shh<`wo$H22H1SUy#&;qf=y zqbr>ma0bnh3PgVD_uugFXCJ3ZJqtQczhYBQ%B2#rHe$Uw0DLO*h0jOCJfz{xcCpR< z1Ia_!<|I?$H5n(m)Z*Ghd<<9Iw++-f+2bW}v|m|6t% z-)z!L+xA6F3hi}~ZC2Mn15~f0bOuaK?I=a4t`%2Bg>5T&ccmJIqD917-(Jdx!yI^a)E$TjQXO=; zU(p90k%cf0niSMm@W_L($}B{KbE!jtG>bCP^=keF~1)1C2fleEund?S8cQ}mQ{)rb9s-= z5odduLtDie73!d^aU*$Hu6H;DyD9$CAZzynpFjC8zi`uLy~*Yxd34sn!b2I-c10yS zQ92d1=y`VV?mv6_3x9HFjSS!9Yyan8{N~^M)f0unW9{mTKl`hneCY4*JF~o;bj?hF zOvyil_&J*nxeW2ib-9pY_tjd|zI0bAMWo|ry;k!|4=zIoufIGy4(C+#CI=nu=3DwrJIJI-F^4U z+6z3$c3T~+imaYAs$C{6;F>F?))W8!_i!vqz2S!XTMg+aLvMp87B774Fa8=?5#3)o zSL^DV|3`b{%Rm0+r@+>L!3V8{+|HkQ`X?WCn`Z~Dtsi~&!*m3D#l4?fJO34$N!QdF ze@J)@9q9DR_ReHT;8g2rZZ9Lu%ws!tqT@@LSDh^z6Bq<)`V$d{`vQD&W3@sFfeAQ< zSOW{%ZRD+3YV}c*xv+Lj1;d)bcmgXb=11_yr_!ry6@m{1XZoL3L0o@{yR;fgKW1rK zOPJK`9EcCxKtxeNKCGGfWOY?~PE;OK5)!vewli>@+wQymJKua78xmuy6qSnPLkz@1 z=3*9tx7-|11a##3+5FkflxvimZCX_F%+$qZT>D~&xrH54&|J+j<&P$P{xagW!};fm zLsmk@2jWKh%1W*mM13=war23kKK9j z?2pa`agUT_X*j%8X*X-z!ysoVKd$P*{i!$>6>OlTmZm1nHp}fcSVf^FVDD$%^@@M_ z~=h1)4>E)72!f+1DLYI zB9iRcJ9YU|o=Y}Lt@v2nP$GmRqe^EeH&a$F6}sV#mGaX9-_=z{{X1OOf5ZC${0Ia0 zzV@|$emlr}%Chs#(qx*pAWb@*`PthqEq+65VNg=9AU3aNVufE-$J~ zvmc%Qioi2P4#<)So9uQZ*&H_98zIW3_1<59`{Jn!Wc3kiH+0)wy;mJLsA{WmSxELu zRUuV@CM}3$NzMD&OB+t$cf}IWz%TMgB+J8?4LGyd9WhD>!;!KqxY|3DZbmJf3`DBb zgKg?9H~Xy`sEfI_fr2L_JMRAWn zOkz=VaZttc5mo@YP-^#bA)bcL9so-5Qjx^gy>t)BgzJK z;}l1bndj>0tkiHcm!ch(Ccf}N#EL_e`|-^KGWM^d5G&z9mwTl_K&Yl$Q<#Z#X70DU+W+ zeV&)%K)v&(sWYoYj2e1uail5nL{$Xh+OXrHyrv1J*g{Elz2IaCbCX5H(j<}~T zF#wmLk7Lo6?o_A$+h-G12*q6>H9;Yhac?qiPmlXx0QqW{SWxv3WUuJHt%6C-M1j~x zassmuMC5Ib19yTO!^;vrCtScy6&kW>Bdy>haZ`w=WX-xlZ4y8!=SNj9ii1^_1g;;o zB=MYRG|NDllzaZ<3n#wx`OPir9MP#J9x5muc_kK+1vmM`sZ@a~Fm0qsO>U7Y~^rSrk~xG!MP zK6r2f;T5uuLcGGOLwr?SHpuNBx4YAu^*rrT$|Gx|-lEZY9MXHWgXen7(K(O-&d5rm zW99Y*ma zyKw#39)I@EN51n{VG2oRl5rOyBVC~R0BiM|u4$ZKZQlLbKYsqjA8t3B7_9Zxs;EE1 zA)XkSV2-ZaQ()(!_^@&45-Dwou|b@Lu(E{;r}_;2%}-5DatV-1g4hFx#?GE8iqz%B z^QG#2Z~f(OedQB+7)uT8x%GCb^Yb5k_ZL6??_BBTR~AA-U zo>)H9d}5`JsH00zgv5%VX#ntOXjD{@vRqa2$dv*pKsJbJ0JCr>yo{TI+HidQ{CD9N zbReG+M3Ttk_>^1HA94U(CeV!5+ZwRa4rvE>JjGJP)ezsNB&B5EKAB_b~c zCrTSN^*m0fX`#jhv4swUT1kuHZfn~~axuv=sC#Hf6mx?jRDMjsSaS<6wN$pRfp%6Pp@TsU)=`$_e{)x`Qqt15#bfAptG@(nA`z8ioG302m}fn z6&~79mLh0Zs4$+}R%YjC8=LRkZccBv%Ibt=q6)`m(pI5io>?m0c<13Wk6zG78}ZCN zj^*T7vRrNJCmi4=n;^4qbs<92D6-=Wng2o_r3Fe!tb$?{o1;|^GZVM&oqF)xdf=*w z!Rz1q`3FAz(_^y}n=AQ@Yfa>m3!5b*DI0g7CLoiz9_XDg+`Cpfjcd&T^CJeD4uH`Y z_#kL(D6CM2IbzJAHtv4?4d4ITD2|*AV>AqOa>v1qmGh*?NdBdRu+psCmR1CF zJ2R8m+-gnlo?O4Qb!Gop89od;4yf0d>-^jY-bi)pcmDBvB)|`312vj7tR0iyj~40# z4=~1}C|RikQ!?S8u~wkH^R?BorT~~dR(diTFG;re;GrYy%jal=)QiSB7|?TdaNPH>8|7RtEZQ`ZEl>?+nSrt7MIE#xkght zUR)+#JP}vxnk&bW$ooqFKj}YTy{40aiK6MqB*}LKnLvme9TjuBX{Qd`5?G?G2_v

^~7l!X(e0+WFW8g`g*5gq-JIu2OzfN zSYakJ{k4;i-*xzSv9zIDiJ^riNDR$K^XHCRJ33h}>3P5T2Y>b#ANqrcK4VYCL)M>R z7HG`1MpMn0D^(|*y5Y71XCGh0TkUk=EGw7IKp|RM2)My+CquI4F3!JLIW~^a?|=9Q z|Km4a4W8{LJb(bSYgv}oIDBljda3Bx2!`V4ZnK|EWuOn$x*+P^T;Tuzkt{=%s5^Fh P00000NkvXXu0mjfIyy-X literal 0 HcmV?d00001 diff --git a/tools/tiledpalettequant/enums.js b/tools/tiledpalettequant/enums.js new file mode 100644 index 00000000..6a2f6ffd --- /dev/null +++ b/tools/tiledpalettequant/enums.js @@ -0,0 +1,32 @@ +// make sure these enums are syncronized with the ones in worker.ts +export var Action; +(function (Action) { + Action[Action["StartQuantization"] = 0] = "StartQuantization"; + Action[Action["UpdateProgress"] = 1] = "UpdateProgress"; + Action[Action["UpdateQuantizedImage"] = 2] = "UpdateQuantizedImage"; + Action[Action["UpdatePalettes"] = 3] = "UpdatePalettes"; + Action[Action["DoneQuantization"] = 4] = "DoneQuantization"; +})(Action || (Action = {})); +export var ColorZeroBehaviour; +(function (ColorZeroBehaviour) { + ColorZeroBehaviour[ColorZeroBehaviour["Unique"] = 0] = "Unique"; + ColorZeroBehaviour[ColorZeroBehaviour["Shared"] = 1] = "Shared"; + ColorZeroBehaviour[ColorZeroBehaviour["TransparentFromTransparent"] = 2] = "TransparentFromTransparent"; + ColorZeroBehaviour[ColorZeroBehaviour["TransparentFromColor"] = 3] = "TransparentFromColor"; +})(ColorZeroBehaviour || (ColorZeroBehaviour = {})); +export var Dither; +(function (Dither) { + Dither[Dither["Off"] = 0] = "Off"; + Dither[Dither["Fast"] = 1] = "Fast"; + Dither[Dither["Slow"] = 2] = "Slow"; +})(Dither || (Dither = {})); +export var DitherPattern; +(function (DitherPattern) { + DitherPattern[DitherPattern["Diagonal4"] = 0] = "Diagonal4"; + DitherPattern[DitherPattern["Horizontal4"] = 1] = "Horizontal4"; + DitherPattern[DitherPattern["Vertical4"] = 2] = "Vertical4"; + DitherPattern[DitherPattern["Diagonal2"] = 3] = "Diagonal2"; + DitherPattern[DitherPattern["Horizontal2"] = 4] = "Horizontal2"; + DitherPattern[DitherPattern["Vertical2"] = 5] = "Vertical2"; +})(DitherPattern || (DitherPattern = {})); +//# sourceMappingURL=enums.js.map \ No newline at end of file diff --git a/tools/tiledpalettequant/index.html b/tools/tiledpalettequant/index.html new file mode 100644 index 00000000..5cf4a49d --- /dev/null +++ b/tools/tiledpalettequant/index.html @@ -0,0 +1,161 @@ + + + + + + + + + Tiled Palette Quantization + + + +

+ + + \ No newline at end of file diff --git a/tools/tiledpalettequant/script.js b/tools/tiledpalettequant/script.js new file mode 100644 index 00000000..bc797f1b --- /dev/null +++ b/tools/tiledpalettequant/script.js @@ -0,0 +1,319 @@ +import { Action, ColorZeroBehaviour, Dither, DitherPattern } from "./enums.js"; +const body = document.getElementById("body"); +const imageSelector = document.getElementById("image_selector"); +const tileWidthInput = document.getElementById("tile_width"); +const tileHeightInput = document.getElementById("tile_height"); +const numPalettesInput = document.getElementById("palette_num"); +const colorsPerPaletteInput = document.getElementById("colors_per_palette"); +const bitsPerChannelInput = document.getElementById("bits_per_channel"); +const fractionOfPixelsInput = document.getElementById("fraction_of_pixels"); +const integerInputs = [ + [tileWidthInput, 8], + [tileHeightInput, 8], + [numPalettesInput, 8], + [colorsPerPaletteInput, 4], + [bitsPerChannelInput, 5], +]; +function validateIntegerInput(numberInput) { + const [inputElement, defaultValue] = numberInput; + let num = parseInt(inputElement.value, radix); + if (isNaN(num)) + num = defaultValue; + const min = parseInt(inputElement.min, radix); + const max = parseInt(inputElement.max, radix); + if (num < min) + num = min; + if (num > max) + num = max; + inputElement.value = num.toString(); +} +function validateFloatInput(numberInput) { + const [inputElement, defaultValue] = numberInput; + let num = parseFloat(inputElement.value); + if (isNaN(num)) + num = defaultValue; + const min = parseFloat(inputElement.min); + const max = parseFloat(inputElement.max); + if (num < min) + num = min; + if (num > max) + num = max; + inputElement.value = num.toFixed(2); +} +const uniqueInput = document.getElementById("unique"); +const sharedInput = document.getElementById("shared"); +const transparentFromTransparentInput = document.getElementById("transparent_from_transparent"); +const transparentFromColorInput = document.getElementById("transparent_from_color"); +const indexZeroButtons = [ + uniqueInput, + sharedInput, + transparentFromTransparentInput, + transparentFromColorInput, +]; +const indexZeroValues = [ + ColorZeroBehaviour.Unique, + ColorZeroBehaviour.Shared, + ColorZeroBehaviour.TransparentFromTransparent, + ColorZeroBehaviour.TransparentFromColor, +]; +const colorZeroAbbreviations = ["u", "s", "t", "tc"]; +const sharedColorInput = document.getElementById("shared_color"); +const transparentColorInput = document.getElementById("transparent_color"); +const defaultColorInput = document.createElement("input"); +defaultColorInput.value = "#000000"; +const colorValues = [ + defaultColorInput, + sharedColorInput, + transparentColorInput, + transparentColorInput, +]; +const ditherOffInput = document.getElementById("dither_off"); +const ditherFastInput = document.getElementById("dither_fast"); +const ditherSlowInput = document.getElementById("dither_slow"); +const ditherButtons = [ditherOffInput, ditherFastInput, ditherSlowInput]; +const ditherValues = [Dither.Off, Dither.Fast, Dither.Slow]; +const ditherWeightInput = document.getElementById("dither_weight"); +const ditherDiagonal4Input = document.getElementById("dither_diagonal4"); +const ditherHorizontal4Input = document.getElementById("dither_horizontal4"); +const ditherVertical4Input = document.getElementById("dither_vertical4"); +const ditherDiagonal2Input = document.getElementById("dither_diagonal2"); +const ditherHorizontal2Input = document.getElementById("dither_horizontal2"); +const ditherVertical2Input = document.getElementById("dither_vertical2"); +const ditherPatternButtons = [ + ditherDiagonal4Input, + ditherHorizontal4Input, + ditherVertical4Input, + ditherDiagonal2Input, + ditherHorizontal2Input, + ditherVertical2Input, +]; +const ditherPatternValues = [ + DitherPattern.Diagonal4, + DitherPattern.Horizontal4, + DitherPattern.Vertical4, + DitherPattern.Diagonal2, + DitherPattern.Horizontal2, + DitherPattern.Vertical2, +]; +let sourceImageName = "carina"; +let sourceImage = document.getElementById("source_img"); +body.addEventListener("dragover", (event) => { + event.preventDefault(); + if (event.dataTransfer == null) + return; + event.dataTransfer.dropEffect = "move"; +}); +body.addEventListener("drop", (event) => { + event.preventDefault(); + const dt = event.dataTransfer; + if (dt == null) + return; + if (dt.files.length > 0) { + const file = dt.files[0]; + if (file.type.substring(0, 6) === "image/") { + sourceImageName = file.name.substring(0, file.name.lastIndexOf(".")); + sourceImage.src = URL.createObjectURL(file); + } + } +}); +imageSelector.addEventListener("change", () => { + if (imageSelector.files == null) + return; + if (imageSelector.files.length > 0) { + const file = imageSelector.files[0]; + sourceImageName = file.name.substring(0, file.name.lastIndexOf(".")); + sourceImage.src = URL.createObjectURL(file); + } +}); +let inProgress = false; +let quantizedImageDownload = document.createElement("a"); +let palettesImageDownload = document.createElement("a"); +let quantizedImage = document.createElement("canvas"); +let palettesImage = document.createElement("canvas"); +let worker = null; +const quantizeButton = document.getElementById("quantizeButton"); +const quantizedImages = document.getElementById("quantized_images"); +const progress = document.getElementById("progress"); +const radix = 10; +quantizeButton.addEventListener("click", () => { + sourceImage = document.getElementById("source_img"); + if (!inProgress) { + inProgress = true; + quantizedImage = document.createElement("canvas"); + quantizedImage.width = sourceImage.width; + quantizedImage.height = sourceImage.height; + quantizedImage.style.marginTop = "8px"; + quantizedImage.style.marginLeft = "8px"; + quantizedImageDownload = document.createElement("a"); + quantizedImageDownload.appendChild(quantizedImage); + palettesImage = document.createElement("canvas"); + palettesImage.width = 16; + palettesImage.height = sourceImage.height; + palettesImage.style.marginTop = "8px"; + palettesImage.style.marginLeft = "8px"; + palettesImageDownload = document.createElement("a"); + palettesImageDownload.appendChild(palettesImage); + const div = document.createElement("div"); + div.appendChild(quantizedImageDownload); + div.appendChild(palettesImageDownload); + quantizedImages.prepend(div); + } + integerInputs.forEach(validateIntegerInput); + validateFloatInput([fractionOfPixelsInput, 0.1]); + validateFloatInput([ditherWeightInput, 0.5]); + const colorZeroBehaviour = selectedValue(indexZeroButtons, indexZeroValues); + const colorInput = selectedValue(indexZeroButtons, colorValues); + const colorZeroValue = hexToColor(colorInput.value); + const ditherMethod = selectedValue(ditherButtons, ditherValues); + const ditherPattern = selectedValue(ditherPatternButtons, ditherPatternValues); + const colorZeroAbbreviation = selectedValue(indexZeroButtons, colorZeroAbbreviations); + const settingsStr = `-${tileWidthInput.value}x${tileHeightInput.value}-${numPalettesInput.value}p${colorsPerPaletteInput.value}c-${colorZeroAbbreviation}`; + const totalPaletteColors = parseInt(numPalettesInput.value, radix) * + parseInt(colorsPerPaletteInput.value, radix); + if (totalPaletteColors > 256) { + quantizedImageDownload.download = + sourceImageName + settingsStr + ".png"; + } + else { + quantizedImageDownload.download = + sourceImageName + settingsStr + ".bmp"; + } + palettesImageDownload.download = + sourceImageName + settingsStr + "-palette.png"; + if (worker) + worker.terminate(); + worker = new Worker("./worker.js"); + worker.onmessage = function (event) { + const data = event.data; + if (data.action === Action.UpdateProgress) { + progress.value = data.progress; + } + else if (data.action === Action.DoneQuantization) { + inProgress = false; + } + else if (data.action === Action.UpdateQuantizedImage) { + const imageData = data.imageData; + const quantizedImageData = new window.ImageData(imageData.width, imageData.height); + for (let i = 0; i < imageData.data.length; i++) { + quantizedImageData.data[i] = imageData.data[i]; + } + quantizedImage.width = imageData.width; + quantizedImage.height = imageData.height; + const ctx = quantizedImage.getContext("2d"); + ctx.putImageData(quantizedImageData, 0, 0); + if (imageData.totalPaletteColors > 256) { + quantizedImageDownload.href = quantizedImage.toDataURL(); + } + else { + quantizedImageDownload.href = bmpToDataURL(imageData.width, imageData.height, imageData.paletteData, imageData.colorIndexes); + } + } + else if (data.action === Action.UpdatePalettes) { + const palettes = data.palettes; + const paletteDisplayHeight = 16; + const paletteDisplayWidth = Math.min(16, Math.ceil(512 / data.numColors)); + palettesImage.width = data.numColors * paletteDisplayWidth; + palettesImage.height = data.numPalettes * paletteDisplayHeight; + const palCtx = palettesImage.getContext("2d"); + for (let j = 0; j < palettes.length; j += 1) { + for (let i = 0; i < palettes[j].length; i += 1) { + palCtx.fillStyle = `rgb( + ${Math.round(palettes[j][i][0])}, + ${Math.round(palettes[j][i][1])}, + ${Math.round(palettes[j][i][2])})`; + palCtx.fillRect(i * paletteDisplayWidth, j * paletteDisplayHeight, paletteDisplayWidth, paletteDisplayHeight); + } + } + palettesImageDownload.href = palettesImage.toDataURL(); + } + }; + worker.postMessage({ + action: Action.StartQuantization, + imageData: imageDataFrom(sourceImage), + quantizationOptions: { + tileWidth: parseInt(tileWidthInput.value, radix), + tileHeight: parseInt(tileHeightInput.value, radix), + numPalettes: parseInt(numPalettesInput.value, radix), + colorsPerPalette: parseInt(colorsPerPaletteInput.value, radix), + bitsPerChannel: parseInt(bitsPerChannelInput.value, radix), + fractionOfPixels: parseFloat(fractionOfPixelsInput.value), + colorZeroBehaviour: colorZeroBehaviour, + colorZeroValue: colorZeroValue, + dither: ditherMethod, + ditherWeight: parseFloat(ditherWeightInput.value), + ditherPattern: ditherPattern, + }, + }); +}); +function hexToColor(colorStr) { + return [ + parseInt(colorStr.slice(1, 3), 16), + parseInt(colorStr.slice(3, 5), 16), + parseInt(colorStr.slice(5, 7), 16), + ]; +} +function selectedValue(radioInputs, values) { + for (let i = 0; i < radioInputs.length; i++) { + if (radioInputs[i].checked) { + return values[i]; + } + } + throw "No radio inputs selected"; +} +function imageDataFrom(img) { + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + canvas.width = img.width; + canvas.height = img.height; + context.drawImage(img, 0, 0); + return context.getImageData(0, 0, img.width, img.height); +} +function bmpToDataURL(width, height, paletteData, colorIndexes) { + const bmpFileSize = 54 + paletteData.length + colorIndexes.length; + const bmpData = new Uint8ClampedArray(bmpFileSize); + bmpData[0] = 66; + bmpData[1] = 77; + write32Le(bmpData, 2, bmpFileSize); + write32Le(bmpData, 6, 0); + write32Le(bmpData, 0xa, 54 + paletteData.length); + write32Le(bmpData, 0xe, 40); + write32Le(bmpData, 0x12, width); + write32Le(bmpData, 0x16, height); + write16Le(bmpData, 0x1a, 1); + write16Le(bmpData, 0x1c, 8); + write32Le(bmpData, 0x1e, 0); + write32Le(bmpData, 0x22, colorIndexes.length); + write32Le(bmpData, 0x26, 2835); + write32Le(bmpData, 0x2a, 2835); + write32Le(bmpData, 0x2e, 256); + write32Le(bmpData, 0x32, 0); + for (let i = 0; i < paletteData.length; i++) { + bmpData[i + 54] = paletteData[i]; + } + const imageDataAddress = 54 + paletteData.length; + for (let i = 0; i < colorIndexes.length; i++) { + bmpData[i + imageDataAddress] = colorIndexes[i]; + } + return "data:image/bmp;base64," + uint8ToBase64(bmpData); +} +function uint8ToBase64(arr) { + return btoa(Array(arr.length) + .fill("") + .map((_, i) => String.fromCharCode(arr[i])) + .join("")); +} +function write32Le(bmpData, index, value) { + bmpData[index] = value % 256; + value = Math.floor(value / 256); + bmpData[index + 1] = value % 256; + value = Math.floor(value / 256); + bmpData[index + 2] = value % 256; + value = Math.floor(value / 256); + bmpData[index + 3] = value % 256; +} +function write16Le(bmpData, index, value) { + bmpData[index] = value % 256; + value = Math.floor(value / 256); + bmpData[index + 1] = value % 256; +} +//# sourceMappingURL=script.js.map \ No newline at end of file diff --git a/tools/tiledpalettequant/style.css b/tools/tiledpalettequant/style.css new file mode 100644 index 00000000..ea71925f --- /dev/null +++ b/tools/tiledpalettequant/style.css @@ -0,0 +1,27 @@ +img { + image-rendering: pixelated; +} + +canvas { + image-rendering: pixelated; +} + +h2 { + font-size: 14px; + margin: 0 0 8px 0; + padding: 0; +} + +body { + font-family: Open Sans, sans-serif; + font-size: 14px; + color: black; + background: #ededed; + display: flex; + display: -webkit-flex; + align-items: center; + -webkit-align-items: center; + flex-direction: column; + -webkit-flex-direction: column; +} + diff --git a/tools/tiledpalettequant/worker.js b/tools/tiledpalettequant/worker.js new file mode 100644 index 00000000..d4b92d31 --- /dev/null +++ b/tools/tiledpalettequant/worker.js @@ -0,0 +1,1243 @@ +"use strict"; +// we can't import these enums from enums.js, because worker modules are not supported in Firefox +var Action; +(function (Action) { + Action[Action["StartQuantization"] = 0] = "StartQuantization"; + Action[Action["UpdateProgress"] = 1] = "UpdateProgress"; + Action[Action["UpdateQuantizedImage"] = 2] = "UpdateQuantizedImage"; + Action[Action["UpdatePalettes"] = 3] = "UpdatePalettes"; + Action[Action["DoneQuantization"] = 4] = "DoneQuantization"; +})(Action || (Action = {})); +var ColorZeroBehaviour; +(function (ColorZeroBehaviour) { + ColorZeroBehaviour[ColorZeroBehaviour["Unique"] = 0] = "Unique"; + ColorZeroBehaviour[ColorZeroBehaviour["Shared"] = 1] = "Shared"; + ColorZeroBehaviour[ColorZeroBehaviour["TransparentFromTransparent"] = 2] = "TransparentFromTransparent"; + ColorZeroBehaviour[ColorZeroBehaviour["TransparentFromColor"] = 3] = "TransparentFromColor"; +})(ColorZeroBehaviour || (ColorZeroBehaviour = {})); +var Dither; +(function (Dither) { + Dither[Dither["Off"] = 0] = "Off"; + Dither[Dither["Fast"] = 1] = "Fast"; + Dither[Dither["Slow"] = 2] = "Slow"; +})(Dither || (Dither = {})); +var DitherPattern; +(function (DitherPattern) { + DitherPattern[DitherPattern["Diagonal4"] = 0] = "Diagonal4"; + DitherPattern[DitherPattern["Horizontal4"] = 1] = "Horizontal4"; + DitherPattern[DitherPattern["Vertical4"] = 2] = "Vertical4"; + DitherPattern[DitherPattern["Diagonal2"] = 3] = "Diagonal2"; + DitherPattern[DitherPattern["Horizontal2"] = 4] = "Horizontal2"; + DitherPattern[DitherPattern["Vertical2"] = 5] = "Vertical2"; +})(DitherPattern || (DitherPattern = {})); +const ditherPatterns = new Map(); +ditherPatterns.set(DitherPattern.Diagonal4, [ + [0, 2], + [3, 1], +]); +ditherPatterns.set(DitherPattern.Horizontal4, [ + [0, 3], + [1, 2], +]); +ditherPatterns.set(DitherPattern.Vertical4, [ + [0, 1], + [3, 2], +]); +ditherPatterns.set(DitherPattern.Diagonal2, [ + [0, 1], + [1, 0], +]); +ditherPatterns.set(DitherPattern.Horizontal2, [ + [0, 1], + [0, 1], +]); +ditherPatterns.set(DitherPattern.Vertical2, [ + [0, 0], + [1, 1], +]); +let ditherPattern = ditherPatterns.get(DitherPattern.Diagonal4); +let ditherPixels = 4; +let quantizationOptions = { + tileWidth: 8, + tileHeight: 8, + numPalettes: 8, + colorsPerPalette: 4, + bitsPerChannel: 5, + fractionOfPixels: 0.1, + colorZeroBehaviour: ColorZeroBehaviour.Unique, + colorZeroValue: [0, 0, 0], + dither: Dither.Off, + ditherWeight: 0.5, + ditherPattern: DitherPattern.Diagonal4, +}; +onmessage = function (event) { + updateProgress(0); + const data = event.data; + quantizationOptions = data.quantizationOptions; + ditherPattern = ditherPatterns.get(quantizationOptions.ditherPattern); + const patternPixels2 = new Set([ + DitherPattern.Diagonal2, + DitherPattern.Horizontal2, + DitherPattern.Vertical2, + ]); + if (patternPixels2.has(quantizationOptions.ditherPattern)) { + ditherPixels = 2; + } + quantizeImage(data.imageData); + updateProgress(100); + postMessage({ action: Action.DoneQuantization }); +}; +function updateProgress(progress) { + postMessage({ action: Action.UpdateProgress, progress: progress }); +} +function updateQuantizedImage(image) { + postMessage({ action: Action.UpdateQuantizedImage, imageData: image }); +} +function updatePalettes(palettes, doSorting) { + let pal = structuredClone(palettes); + const colorZeroBehaviour = quantizationOptions.colorZeroBehaviour; + let startIndex = 0; + if (colorZeroBehaviour === ColorZeroBehaviour.TransparentFromColor || + colorZeroBehaviour === ColorZeroBehaviour.TransparentFromTransparent) { + startIndex = 1; + for (const palette of pal) { + palette.unshift(cloneColor(quantizationOptions.colorZeroValue)); + } + } + if (colorZeroBehaviour === ColorZeroBehaviour.Shared) { + startIndex = 1; + } + if (doSorting) { + pal = sortPalettes(pal, startIndex); + } + postMessage({ + action: Action.UpdatePalettes, + palettes: pal, + numPalettes: quantizationOptions.numPalettes, + numColors: quantizationOptions.colorsPerPalette, + }); +} +function movePalettesCloser(palettes, pixel, alpha) { + let sharedColorIndex = -1; + if (quantizationOptions.colorZeroBehaviour === ColorZeroBehaviour.Shared) { + sharedColorIndex = 0; + } + let closestPaletteIndex = -1; + let closestColorIndex = -1; + let targetColor; + if (quantizationOptions.dither === Dither.Slow) { + closestPaletteIndex = getClosestPaletteIndexDither(palettes, pixel.tile); + [closestColorIndex, , targetColor] = getClosestColorDither(palettes[closestPaletteIndex], pixel); + } + else { + closestPaletteIndex = getClosestPaletteIndex(palettes, pixel.tile); + [closestColorIndex] = getClosestColor(palettes[closestPaletteIndex], pixel.color); + targetColor = pixel.color; + } + if (closestColorIndex !== sharedColorIndex) { + moveColorCloser(palettes[closestPaletteIndex][closestColorIndex], targetColor, alpha); + } +} +function quantizeImage(image) { + console.log(quantizationOptions); + const t0 = performance.now(); + const reducedImageData = { + width: image.width, + height: image.height, + data: new Uint8ClampedArray(image.data.length), + }; + const useDither = quantizationOptions.dither !== Dither.Off; + if (useDither) { + for (let i = 0; i < image.data.length; i++) { + reducedImageData.data[i] = image.data[i]; + } + } + else { + for (let i = 0; i < image.data.length; i++) { + reducedImageData.data[i] = toNbit(image.data[i], quantizationOptions.bitsPerChannel); + } + } + const tiles = extractTiles(reducedImageData); + let avgPixelsPerTile = 0; + for (const tile of tiles) { + avgPixelsPerTile += tile.colors.length; + } + avgPixelsPerTile /= tiles.length; + console.log("Colors per tile: " + avgPixelsPerTile.toFixed(2)); + const pixels = extractAllPixels(tiles); + const randomShuffle = new RandomShuffle(pixels.length); + const showProgress = true; + let iterations = quantizationOptions.fractionOfPixels * pixels.length; + let alpha = 0.3; + let finalAlpha = 0.05; + const meanSquareErr = meanSquareError; + if (quantizationOptions.dither === Dither.Slow) { + // meanSquareErr = meanSquareErrorDither; + iterations /= 5; + alpha = 0.1; + finalAlpha = 0.02; + } + const minColorFactor = 0.5; + const minPaletteFactor = 0.5; + const replaceIterations = 10; + const useMin = true; + const prog = [25, 65, 90, 100]; + if (quantizationOptions.dither != Dither.Off) { + prog[3] = 94; + } + let palettes = colorQuantize1Color(tiles, pixels, randomShuffle); + let startIndex = 2; + if (quantizationOptions.colorZeroBehaviour === ColorZeroBehaviour.Shared) { + startIndex += 1; + } + let endIndex = quantizationOptions.colorsPerPalette; + if (quantizationOptions.colorZeroBehaviour === + ColorZeroBehaviour.TransparentFromColor || + quantizationOptions.colorZeroBehaviour === + ColorZeroBehaviour.TransparentFromTransparent) { + endIndex -= 1; + } + updateProgress(prog[0] / quantizationOptions.numPalettes); + updatePalettes(palettes, false); + if (showProgress) + updateQuantizedImage(quantizeTiles(palettes, reducedImageData, false)); + for (let numColors = startIndex; numColors <= endIndex; numColors++) { + expandPalettesByOneColor(palettes, tiles, pixels, randomShuffle); + updateProgress((prog[0] * numColors) / quantizationOptions.colorsPerPalette); + updatePalettes(palettes, false); + if (showProgress) + updateQuantizedImage(quantizeTiles(palettes, reducedImageData, false)); + } + let minMse = meanSquareErr(palettes, tiles); + let minPalettes = structuredClone(palettes); + for (let i = 0; i < replaceIterations; i++) { + palettes = replaceWeakestColors(palettes, tiles, minColorFactor, minPaletteFactor, true); + for (let iteration = 0; iteration < iterations; iteration++) { + const nextPixel = pixels[randomShuffle.next()]; + movePalettesCloser(palettes, nextPixel, alpha); + } + const mse = meanSquareErr(palettes, tiles); + if (mse < minMse) { + minMse = mse; + minPalettes = structuredClone(palettes); + } + updateProgress(prog[0] + ((prog[1] - prog[0]) * (i + 1)) / replaceIterations); + updatePalettes(palettes, false); + if (showProgress) { + if (useMin && i === replaceIterations - 1) { + updateQuantizedImage(quantizeTiles(minPalettes, reducedImageData, false)); + } + else { + updateQuantizedImage(quantizeTiles(palettes, reducedImageData, false)); + } + } + console.log("MSE: " + mse.toFixed(0)); + // console.log((performance.now() - t1).toFixed(0) + " ms"); + } + if (useMin) { + palettes = minPalettes; + } + if (!useDither) + palettes = reducePalettes(palettes, quantizationOptions.bitsPerChannel); + const finalIterations = iterations * 10; + let nextUpdate = iterations; + for (let iteration = 0; iteration < finalIterations; iteration++) { + const nextPixel = pixels[randomShuffle.next()]; + movePalettesCloser(palettes, nextPixel, finalAlpha); + if (iteration >= nextUpdate) { + nextUpdate += iterations; + updateProgress(prog[1] + ((prog[2] - prog[1]) * iteration) / finalIterations); + updatePalettes(palettes, false); + } + } + console.log("Normal final: " + meanSquareError(palettes, tiles).toFixed(0)); + console.log("Dither final: " + meanSquareErrorDither(palettes, tiles).toFixed(0)); + updateProgress(prog[2]); + updatePalettes(palettes, false); + if (!useDither) { + palettes = reducePalettes(palettes, quantizationOptions.bitsPerChannel); + for (let i = 0; i < 3; i++) { + palettes = kMeans(palettes, tiles); + updateProgress(prog[2] + ((prog[3] - prog[2]) * (i + 1)) / 3); + updatePalettes(palettes, false); + } + } + palettes = reducePalettes(palettes, quantizationOptions.bitsPerChannel); + updatePalettes(palettes, true); + updateQuantizedImage(quantizeTiles(palettes, reducedImageData, useDither)); + console.log("> MSE: " + meanSquareError(palettes, tiles).toFixed(2)); + console.log(`> Time: ${((performance.now() - t0) / 1000).toFixed(2)} sec`); +} +function reducePalettes(palettes, bitsPerChannel) { + const result = []; + for (const palette of palettes) { + const pal = []; + for (const color of palette) { + const col = cloneColor(color); + toNbitColor(col, bitsPerChannel); + pal.push(col); + } + result.push(pal); + } + return result; +} +function sortPalettes(palettes, startIndex) { + const pairIterations = 2000; + const tIterations = 10000; + const paletteIterations = 100000; + const upWeight = 2; + const numPalettes = palettes.length; + const numColors = palettes[0].length; + if (numColors === 2 && startIndex === 1) { + return palettes; + } + // paletteDist[i+1][j+1] stores distance between palette i and palette j + const paletteDist = zeros2(numPalettes + 2, numPalettes + 2); + // colorIndex[p1][p2][i] stores the index of the closest color in p2 from color index i in p1 + const colorIndex = zeros3(numPalettes, numPalettes, numColors); + for (let i = 0; i < numPalettes; i++) { + for (let j = 0; j < numPalettes; j++) { + for (let k = 0; k < numColors; k++) { + colorIndex[i][j][k] = k; + } + } + } + for (let p1 = 0; p1 < numPalettes - 1; p1++) { + for (let p2 = p1 + 1; p2 < numPalettes; p2++) { + const index = colorIndex[p1][p2]; + for (let iteration = 0; iteration < pairIterations; iteration++) { + let i1 = startIndex + + Math.floor(Math.random() * (numColors - startIndex - 1)); + let i2 = i1 + 1 + Math.floor(Math.random() * (numColors - i1 - 1)); + if (Math.random() < 0.5) { + [i1, i2] = [i2, i1]; + } + const p1i1 = palettes[p1][i1]; + const p1i2 = palettes[p1][i2]; + const p2i1 = palettes[p2][index[i1]]; + const p2i2 = palettes[p2][index[i2]]; + const straightDist = colorDistance(p1i1, p2i1) + colorDistance(p1i2, p2i2); + const swappedDist = colorDistance(p1i1, p2i2) + colorDistance(p1i2, p2i1); + if (swappedDist < straightDist) { + [index[i1], index[i2]] = [index[i2], index[i1]]; + } + } + let sum = 0; + for (let i = 0; i < numColors; i++) { + const p1i = palettes[p1][i]; + const p2i = palettes[p2][index[i]]; + sum += colorDistance(p1i, p2i); + } + paletteDist[p1 + 1][p2 + 1] = sum; + paletteDist[p2 + 1][p1 + 1] = sum; + } + } + for (let p1 = 1; p1 < numPalettes; p1++) { + for (let p2 = 0; p2 < p1; p2++) { + const index = colorIndex[p2][p1]; + const revIndex = colorIndex[p1][p2]; + for (let i = 0; i < numColors; i++) { + revIndex[i] = index.indexOf(i); + } + } + } + const palIndex = []; + for (let i = 0; i < numPalettes + 2; i++) { + palIndex.push(i); + } + if (numPalettes > 2) { + for (let iteration = 0; iteration < paletteIterations; iteration++) { + const index1 = Math.max(1, Math.floor(Math.random() * numPalettes)); + const index2 = Math.min(numPalettes, index1 + 1 + Math.floor(Math.random() * numPalettes)); + const i1b = palIndex[index1 - 1]; + const i1 = palIndex[index1]; + const i2 = palIndex[index2]; + const i2b = palIndex[index2 + 1]; + const straightDist = paletteDist[i1b][i1] + paletteDist[i2][i2b]; + const swappedDist = paletteDist[i1b][i2] + paletteDist[i1][i2b]; + if (swappedDist < straightDist) { + reverse(palIndex, index1, index2); + } + } + } + const pal1 = palettes[palIndex[1] - 1]; + const p1Index = []; + for (let i = 0; i < numColors + 2; i++) { + p1Index.push(i); + } + const p1Dist = zeros2(numColors + 2, numColors + 2); + for (let i = 1; i <= numColors; i++) { + for (let j = 1; j <= numColors; j++) { + p1Dist[i][j] = colorDistance(pal1[i - 1], pal1[j - 1]); + } + } + if (numColors > 2) { + for (let iteration = 0; iteration < paletteIterations; iteration++) { + const index1 = Math.max(1 + startIndex, Math.floor(Math.random() * numColors)); + const index2 = Math.min(numColors, index1 + 1 + Math.floor(Math.random() * numColors)); + const i1b = p1Index[index1 - 1]; + const i1 = p1Index[index1]; + const i2 = p1Index[index2]; + const i2b = p1Index[index2 + 1]; + const straightDist = p1Dist[i1b][i1] + p1Dist[i2][i2b]; + const swappedDist = p1Dist[i1b][i2] + p1Dist[i1][i2b]; + if (swappedDist < straightDist) { + reverse(p1Index, index1, index2); + } + } + } + const pIndex = zeros2(numPalettes, numColors); + for (let i = 0; i < numColors; i++) { + pIndex[0][i] = p1Index[i + 1] - 1; + } + for (let i = 1; i < numPalettes; i++) { + for (let j = 0; j < numColors; j++) { + const p1 = palIndex[i] - 1; + const p2 = palIndex[i + 1] - 1; + pIndex[i][j] = colorIndex[p1][p2][pIndex[i - 1][j]]; + } + } + if (numColors >= 4) + for (let i = 1; i < numPalettes; i++) { + const p1 = palIndex[i] - 1; + const p2 = palIndex[i + 1] - 1; + let iteration = 0; + while (iteration < tIterations) { + const index1 = Math.max(startIndex, Math.floor(Math.random() * numColors)); + const index2 = Math.max(startIndex, Math.floor(Math.random() * numColors)); + if (index1 === index2) + continue; + const up1 = pIndex[i - 1][index1]; + const i1 = pIndex[i][index1]; + const left1 = pIndex[i][index1 - 1]; + const right1 = pIndex[i][index1 + 1]; + const up2 = pIndex[i - 1][index2]; + const i2 = pIndex[i][index2]; + const left2 = pIndex[i][index2 - 1]; + const right2 = pIndex[i][index2 + 1]; + let straightDist = upWeight * + colorDistance(palettes[p2][i1], palettes[p1][up1]); + if (left1 >= 0) + straightDist += colorDistance(palettes[p2][i1], palettes[p2][left1]); + if (right1 < numColors) + straightDist += colorDistance(palettes[p2][i1], palettes[p2][right1]); + straightDist += + upWeight * + colorDistance(palettes[p2][i2], palettes[p1][up2]); + if (left2 >= 0) + straightDist += colorDistance(palettes[p2][i2], palettes[p2][left2]); + if (right2 < numColors) + straightDist += colorDistance(palettes[p2][i2], palettes[p2][right2]); + let swappedDist = upWeight * + colorDistance(palettes[p2][i2], palettes[p1][up1]); + if (left1 >= 0) + swappedDist += colorDistance(palettes[p2][i2], palettes[p2][left1]); + if (right1 < numColors) + swappedDist += colorDistance(palettes[p2][i2], palettes[p2][right1]); + swappedDist += + upWeight * + colorDistance(palettes[p2][i1], palettes[p1][up2]); + if (left2 >= 0) + swappedDist += colorDistance(palettes[p2][i1], palettes[p2][left2]); + if (right2 < numColors) + swappedDist += colorDistance(palettes[p2][i1], palettes[p2][right2]); + if (swappedDist < straightDist) { + [pIndex[i][index1], pIndex[i][index2]] = [ + pIndex[i][index2], + pIndex[i][index1], + ]; + } + iteration++; + } + } + const pals = []; + for (let i = 0; i < numPalettes; i++) { + const p2 = palIndex[i + 1] - 1; + const pal = []; + for (let j = 0; j < numColors; j++) { + pal.push(palettes[p2][pIndex[i][j]]); + } + pals.push(pal); + } + return pals; +} +function zeroArray(len) { + const result = []; + for (let i = 0; i < len; i++) { + result.push(0); + } + return result; +} +function zeros2(len1, len2) { + const result = []; + for (let i = 0; i < len1; i++) { + result.push(zeroArray(len2)); + } + return result; +} +function zeros3(len1, len2, len3) { + const result = []; + for (let i = 0; i < len1; i++) { + result.push(zeros2(len2, len3)); + } + return result; +} +function reverse(a, left, right) { + const middle = (left + right) / 2.0; + while (left < middle) { + [a[left], a[right]] = [a[right], a[left]]; + left++; + right--; + } +} +function toLinear(x) { + return x * x; +} +function toLinearColor(color) { + for (let i = 0; i < color.length; i++) { + color[i] = toLinear(color[i]); + } +} +function toSrgb(x) { + return Math.sqrt(x); +} +function toSrgbColor(color) { + for (let i = 0; i < color.length; i++) { + color[i] = toSrgb(color[i]); + } +} +const brightnessScale = [0.299, 0.587, 0.114]; +function brightness(color) { + let sum = 0; + for (let i = 0; i < 3; i++) { + sum += brightnessScale[i] * toLinear(color[i]); + } + return sum; +} +function replaceWeakestColors(palettes, tiles, minColorFactor, minPaletteFactor, replacePalettes) { + const colorZeroBehaviour = quantizationOptions.colorZeroBehaviour; + const useSlowDither = quantizationOptions.dither === Dither.Slow; + let closestPal = closestPaletteDistance; + if (useSlowDither) { + closestPal = closestPaletteDistanceDither; + } + const closestPaletteIndex = zeroArray(tiles.length); + let maxPaletteIndex = 0; + let minPaletteIndex = 0; + const totalPaletteMse = zeroArray(palettes.length); + const removedPaletteMse = zeroArray(palettes.length); + if (palettes.length > 1) { + for (let j = 0; j < tiles.length; j++) { + const tile = tiles[j]; + const [index, minDistance] = closestPal(palettes, tile); + totalPaletteMse[index] += minDistance; + closestPaletteIndex[j] = index; + const remainingPalettes = []; + for (let i = 0; i < palettes.length; i++) { + if (i != index) { + remainingPalettes.push(palettes[i]); + } + } + if (remainingPalettes.length > 0) { + const [, minDistance2] = closestPal(remainingPalettes, tile); + removedPaletteMse[index] += minDistance2; + } + } + maxPaletteIndex = maxIndex(totalPaletteMse); + minPaletteIndex = minIndex(removedPaletteMse); + } + const result = []; + if (palettes[0].length > 1) { + const totalColorMse = []; + const secondColorMse = []; + for (let j = 0; j < palettes.length; j++) { + totalColorMse.push(zeroArray(palettes[j].length)); + secondColorMse.push(zeroArray(palettes[j].length)); + } + for (let j = 0; j < tiles.length; j++) { + const tile = tiles[j]; + const minPaletteIndex = closestPaletteIndex[j]; + const pal = palettes[minPaletteIndex]; + if (useSlowDither) { + for (const pixel of tile.pixels) { + const [minColorIndex, minDist] = getClosestColorDither(pal, pixel); + totalColorMse[minPaletteIndex][minColorIndex] += minDist; + const remainingColors = []; + for (let i = 0; i < pal.length; i++) { + if (i != minColorIndex) { + remainingColors.push(pal[i]); + } + } + const [, secondDist] = getClosestColorDither(remainingColors, pixel); + secondColorMse[minPaletteIndex][minColorIndex] += + secondDist; + } + } + else { + for (let i = 0; i < tile.colors.length; i++) { + const color = tile.colors[i]; + const [minColorIndex, minDist] = getClosestColor(pal, color); + totalColorMse[minPaletteIndex][minColorIndex] += + minDist * tile.counts[i]; + const remainingColors = []; + for (let i = 0; i < pal.length; i++) { + if (i != minColorIndex) { + remainingColors.push(pal[i]); + } + } + const [, secondDist] = getClosestColor(remainingColors, color); + secondColorMse[minPaletteIndex][minColorIndex] += + secondDist * tile.counts[i]; + } + } + } + let sharedColorIndex = -1; + if (colorZeroBehaviour === ColorZeroBehaviour.Shared) { + sharedColorIndex = 0; + } + for (let palIndex = 0; palIndex < palettes.length; palIndex++) { + const maxColorIndex = maxIndex(totalColorMse[palIndex]); + const minColorIndex = minIndex(secondColorMse[palIndex]); + const shouldReplaceMinColor = minColorIndex !== maxColorIndex && + minColorIndex !== sharedColorIndex && + secondColorMse[palIndex][minColorIndex] < + minColorFactor * totalColorMse[palIndex][maxColorIndex]; + const colors = []; + for (let i = 0; i < palettes[palIndex].length; i++) { + if (i == minColorIndex && shouldReplaceMinColor) { + console.log("replaced color in palette " + palIndex); + colors.push(cloneColor(palettes[palIndex][maxColorIndex])); + } + else { + colors.push(cloneColor(palettes[palIndex][i])); + } + } + result.push(colors); + } + } + else { + for (let palIndex = 0; palIndex < palettes.length; palIndex++) { + const colors = []; + for (let i = 0; i < palettes[palIndex].length; i++) { + colors.push(cloneColor(palettes[palIndex][i])); + } + result.push(colors); + } + } + if (replacePalettes && + minPaletteIndex != maxPaletteIndex && + removedPaletteMse[minPaletteIndex] < + minPaletteFactor * totalPaletteMse[maxPaletteIndex]) { + console.log("replaced palette " + minPaletteIndex); + while (result[minPaletteIndex].length > 0) + result[minPaletteIndex].pop(); + for (const color of result[maxPaletteIndex]) { + const c = structuredClone(color); + result[minPaletteIndex].push(c); + } + } + return result; +} +function kMeans(palettes, tiles) { + const colorZeroBehaviour = quantizationOptions.colorZeroBehaviour; + const counts = []; + const sumColors = []; + for (let i = 0; i < palettes.length; i++) { + const c = []; + const colors = []; + for (let j = 0; j < palettes[i].length; j++) { + c.push(0); + colors.push([0, 0, 0]); + } + counts.push(c); + sumColors.push(colors); + } + for (const tile of tiles) { + if (quantizationOptions.dither === Dither.Slow) { + const palIndex = getClosestPaletteIndexDither(palettes, tile); + for (const pixel of tile.pixels) { + const [colIndex, ,] = getClosestColorDither(palettes[palIndex], pixel); + counts[palIndex][colIndex] += 1; + addColor(sumColors[palIndex][colIndex], pixel.color); + } + } + else { + const palIndex = getClosestPaletteIndex(palettes, tile); + for (let i = 0; i < tile.colors.length; i++) { + const [colIndex] = getClosestColor(palettes[palIndex], tile.colors[i]); + counts[palIndex][colIndex] += tile.counts[i]; + const color = cloneColor(tile.colors[i]); + scaleColor(color, tile.counts[i]); + addColor(sumColors[palIndex][colIndex], color); + } + } + } + let sharedColorIndex = -1; + if (colorZeroBehaviour === ColorZeroBehaviour.Shared) { + sharedColorIndex = 0; + } + for (let i = 0; i < sumColors.length; i++) { + for (let j = 0; j < sumColors[i].length; j++) { + if (counts[i][j] == 0 || j === sharedColorIndex) { + sumColors[i][j] = cloneColor(palettes[i][j]); + } + else { + scaleColor(sumColors[i][j], 1.0 / counts[i][j]); + } + } + } + return sumColors; +} +function meanSquareError(palettes, tiles) { + let totalDistance = 0; + let count = 0; + for (const tile of tiles) { + const palIndex = getClosestPaletteIndex(palettes, tile); + for (let i = 0; i < tile.colors.length; i++) { + const [, minDistance] = getClosestColor(palettes[palIndex], tile.colors[i]); + totalDistance += minDistance * tile.counts[i]; + count += tile.counts[i]; + } + } + return totalDistance / count; +} +function meanSquareErrorDither(palettes, tiles) { + let totalDistance = 0; + let count = 0; + for (const tile of tiles) { + const palIndex = getClosestPaletteIndexDither(palettes, tile); + for (const pixel of tile.pixels) { + const [, minDistance] = getClosestColorDither(palettes[palIndex], pixel); + totalDistance += minDistance; + count += 1; + } + } + return totalDistance / count; +} +class RandomShuffle { + constructor(n) { + this.values = []; + for (let i = 0; i < n; i++) { + this.values.push(i); + } + this.currentIndex = n - 1; + } + shuffle() { + for (let i = 0; i < this.values.length; i++) { + const index = i + Math.floor(Math.random() * (this.values.length - i)); + const tmp = this.values[i]; + this.values[i] = this.values[index]; + this.values[index] = tmp; + } + } + next() { + this.currentIndex += 1; + if (this.currentIndex >= this.values.length) { + this.shuffle(); + this.currentIndex = 0; + } + return this.values[this.currentIndex]; + } +} +function getClosestColor(palette, color) { + let minIndex = palette.length - 1; + let minDist = colorDistance(palette[minIndex], color); + for (let i = palette.length - 2; i >= 0; i--) { + const dist = colorDistance(palette[i], color); + if (dist < minDist) { + minIndex = i; + minDist = dist; + } + } + return [minIndex, minDist]; +} +function getClosestColorDither(palette, pixel) { + const error = [0, 0, 0]; + const linearPixel = cloneColor(pixel.color); + toLinearColor(linearPixel); + const candidates = []; + const c = [0, 0, 0]; + const err = [0, 0, 0]; + const reducedColor = [0, 0, 0]; + for (let i = 0; i < ditherPixels; i++) { + copyColor(c, linearPixel); + copyColor(err, error); + scaleColor(err, quantizationOptions.ditherWeight); + addColor(c, err); + clampColor(c, 0, 255 * 255); + toSrgbColor(c); + const [minColorIndex, minDist] = getClosestColor(palette, c); + const minColor = palette[minColorIndex]; + candidates.push({ + colorIndex: minColorIndex, + colorDistance: minDist, + comparedColor: c, + brightness: brightness(minColor), + }); + copyColor(reducedColor, minColor); + toNbitColor(reducedColor, quantizationOptions.bitsPerChannel); + toLinearColor(reducedColor); + addColor(error, linearPixel); + subtractColor(error, reducedColor); + } + for (let i = 0; i < ditherPixels - 1; i++) { + for (let j = i + 1; j < ditherPixels; j++) { + if (candidates[i].brightness > candidates[j].brightness) { + [candidates[i], candidates[j]] = [candidates[j], candidates[i]]; + } + } + } + const index = ditherPattern[pixel.x & 1][pixel.y & 1]; + return [ + candidates[index].colorIndex, + candidates[index].colorDistance, + candidates[index].comparedColor, + ]; +} +function colorDistance(a, b) { + return 2 * Math.pow((a[0] - b[0]), 2) + 4 * Math.pow((a[1] - b[1]), 2) + Math.pow((a[2] - b[2]), 2); +} +function paletteDistance(palette, tile) { + let sum = 0; + const colors = tile.colors; + const counts = tile.counts; + for (let i = 0; i < colors.length; i++) { + const [, minDist] = getClosestColor(palette, colors[i]); + sum += counts[i] * minDist; + } + return sum; +} +function paletteDistanceDither(palette, tile) { + let sum = 0; + for (const pixel of tile.pixels) { + const [, minDist] = getClosestColorDither(palette, pixel); + sum += minDist; + } + return sum; +} +function getClosestPaletteIndex(palettes, tile) { + if (palettes.length === 1) + return 0; + const distances = palettes.map((palette) => paletteDistance(palette, tile)); + return minIndex(distances); +} +function closestPaletteDistance(palettes, tile) { + const distances = palettes.map((palette) => paletteDistance(palette, tile)); + const index = minIndex(distances); + return [index, distances[index]]; +} +function getClosestPaletteIndexDither(palettes, tile) { + if (palettes.length === 1) + return 0; + const distances = palettes.map((palette) => paletteDistanceDither(palette, tile)); + return minIndex(distances); +} +function closestPaletteDistanceDither(palettes, tile) { + const distances = palettes.map((palette) => paletteDistanceDither(palette, tile)); + const index = minIndex(distances); + return [index, distances[index]]; +} +function getColor(image, x, y) { + const index = 4 * (x + image.width * y); + const color = [ + image.data[index], + image.data[index + 1], + image.data[index + 2], + ]; + return color; +} +function extractTile(image, startX, startY) { + const { tileWidth, tileHeight, colorZeroBehaviour, colorZeroValue } = quantizationOptions; + const tile = { + colors: [], + counts: [], + pixels: [], + }; + const endX = Math.min(startX + tileWidth, image.width); + const endY = Math.min(startY + tileHeight, image.height); + for (let y = startY; y < endY; y++) { + for (let x = startX; x < endX; x++) { + const color = getColor(image, x, y); + // skip transparent pixels + if (isColorTransparent(color) || isPixelTransparent(x, y)) { + continue; + } + tile.pixels.push({ tile, color, x, y }); + const colorIndex = tile.colors.findIndex((c) => equalColors(c, color)); + if (colorIndex >= 0) { + tile.counts[colorIndex]++; + } + else { + tile.colors.push(color); + tile.counts.push(1); + } + } + } + return tile; + function isPixelTransparent(x, y) { + const index = 4 * (x + image.width * y); + return (colorZeroBehaviour === + ColorZeroBehaviour.TransparentFromTransparent && + image.data[index + 3] < 255); + } + function isColorTransparent(color) { + return (colorZeroBehaviour === ColorZeroBehaviour.TransparentFromColor && + equalColors(color, colorZeroValue)); + } +} +function extractTiles(image) { + const { tileWidth, tileHeight } = quantizationOptions; + const tiles = []; + let totalPixels = 0; + let tileCount = 0; + for (let y = 0; y < image.height; y += tileHeight) { + for (let x = 0; x < image.width; x += tileWidth) { + const tile = extractTile(image, x, y); + if (tile.colors.length === 0) + continue; + tiles.push(tile); + totalPixels += tile.pixels.length; + tileCount++; + } + } + const avgPixelsPerTile = totalPixels / tileCount; + console.log("avg pixels per tile: " + avgPixelsPerTile.toFixed(2)); + return tiles; +} +function equalColors(c1, c2) { + for (let i = 0; i < c1.length; i++) { + if (c1[i] !== c2[i]) { + return false; + } + } + return true; +} +function extractAllPixels(tiles) { + const pixels = []; + for (const tile of tiles) { + for (const pixel of tile.pixels) { + pixels.push(Object.assign({}, pixel)); + } + } + return pixels; +} +function quantizeTiles(palettes, image, useDither) { + const { tileWidth, tileHeight, bitsPerChannel, colorZeroBehaviour, colorZeroValue, numPalettes, colorsPerPalette, } = quantizationOptions; + const imageIsReduced = quantizationOptions.dither !== Dither.Off; + let adjustedIndex = 0; + if (colorZeroBehaviour === ColorZeroBehaviour.TransparentFromColor || + colorZeroBehaviour === ColorZeroBehaviour.TransparentFromTransparent) { + adjustedIndex = 1; + } + const reducedPalettes = structuredClone(palettes); + for (const pal of reducedPalettes) { + for (const color of pal) { + toNbitColor(color, bitsPerChannel); + } + } + const transparentColor = cloneColor(colorZeroValue); + if (imageIsReduced) + toNbitColor(transparentColor, bitsPerChannel); + const colorZero = cloneColor(colorZeroValue); + toNbitColor(colorZero, bitsPerChannel); + const bmpWidth = Math.ceil(image.width / 4) * 4; + const quantizedImage = { + width: image.width, + height: image.height, + data: new Uint8ClampedArray(image.data.length), + totalPaletteColors: numPalettes * colorsPerPalette, + paletteData: new Uint8ClampedArray(1024), + colorIndexes: new Uint8ClampedArray(bmpWidth * image.height), + }; + if (numPalettes * colorsPerPalette <= 256) { + addBmpColors(reducedPalettes, quantizedImage.paletteData); + } + for (let startY = 0; startY < image.height; startY += tileHeight) { + for (let startX = 0; startX < image.width; startX += tileWidth) { + const tile = extractTile(image, startX, startY); + let palette = reducedPalettes[0]; + let closestPaletteIndex = 0; + if (tile.colors.length > 0) { + if (useDither) { + closestPaletteIndex = getClosestPaletteIndexDither(reducedPalettes, tile); + } + else { + closestPaletteIndex = getClosestPaletteIndex(reducedPalettes, tile); + } + palette = reducedPalettes[closestPaletteIndex]; + } + const endX = Math.min(startX + tileWidth, image.width); + const endY = Math.min(startY + tileHeight, image.height); + for (let y = startY; y < endY; y++) { + for (let x = startX; x < endX; x++) { + const index = 4 * (x + image.width * y); + const bmpIndex = x + bmpWidth * (image.height - 1 - y); + const color = [ + image.data[index], + image.data[index + 1], + image.data[index + 2], + ]; + if ((colorZeroBehaviour === + ColorZeroBehaviour.TransparentFromTransparent && + image.data[index + 3] < 255) || + (colorZeroBehaviour === + ColorZeroBehaviour.TransparentFromColor && + equalColors(color, transparentColor))) { + quantizedImage.data[index + 0] = image.data[index + 0]; + quantizedImage.data[index + 1] = image.data[index + 1]; + quantizedImage.data[index + 2] = image.data[index + 2]; + quantizedImage.data[index + 3] = image.data[index + 3]; + quantizedImage.colorIndexes[bmpIndex] = + closestPaletteIndex * colorsPerPalette; + } + else { + let closestColorIndex = 0; + if (useDither) { + [closestColorIndex] = getClosestColorDither(palette, { + color: color, + x: x, + y: y, + }); + } + else { + [closestColorIndex] = getClosestColor(palette, color); + } + const paletteColor = cloneColor(palette[closestColorIndex]); + quantizedImage.data[index + 0] = paletteColor[0]; + quantizedImage.data[index + 1] = paletteColor[1]; + quantizedImage.data[index + 2] = paletteColor[2]; + quantizedImage.data[index + 3] = 255; + quantizedImage.colorIndexes[bmpIndex] = + closestPaletteIndex * colorsPerPalette + + closestColorIndex + + adjustedIndex; + } + } + } + } + } + return quantizedImage; + function addBmpColors(palettes, bmpPalette) { + let i = 0; + for (const pal of palettes) { + if (adjustedIndex === 1) { + bmpPalette[i] = colorZero[2]; + bmpPalette[i + 1] = colorZero[1]; + bmpPalette[i + 2] = colorZero[0]; + i += 4; + } + for (const color of pal) { + bmpPalette[i] = color[2]; + bmpPalette[i + 1] = color[1]; + bmpPalette[i + 2] = color[0]; + i += 4; + } + } + } +} +function colorQuantize1Color(tiles, pixels, randomShuffle) { + let iterations = quantizationOptions.fractionOfPixels * pixels.length; + let alpha = 0.3; + if (quantizationOptions.dither === Dither.Slow) { + iterations /= 5; + alpha = 0.1; + } + const avgColor = [0, 0, 0]; + for (const pixel of pixels) { + addColor(avgColor, pixel.color); + } + scaleColor(avgColor, 1.0 / pixels.length); + const palettes = [[avgColor]]; + if (quantizationOptions.colorZeroBehaviour === ColorZeroBehaviour.Shared) { + palettes[0].push(avgColor); + palettes[0][0] = structuredClone(quantizationOptions.colorZeroValue); + } + let splitIndex = 0; + for (let numPalettes = 2; numPalettes <= quantizationOptions.numPalettes; numPalettes++) { + palettes.push(structuredClone(palettes[splitIndex])); + for (let iteration = 0; iteration < iterations; iteration++) { + const nextPixel = pixels[randomShuffle.next()]; + movePalettesCloser(palettes, nextPixel, alpha); + } + const paletteDistance = zeroArray(numPalettes); + for (const tile of tiles) { + const [palIndex, distance] = closestPaletteDistance(palettes, tile); + paletteDistance[palIndex] += distance; + } + splitIndex = maxIndex(paletteDistance); + } + return palettes; +} +function expandPalettesByOneColor(palettes, tiles, pixels, randomShuffle) { + let iterations = quantizationOptions.fractionOfPixels * pixels.length; + let alpha = 0.3; + if (quantizationOptions.dither === Dither.Slow) { + iterations /= 5; + alpha = 0.1; + } + const numColors = palettes[0].length + 1; + const splitIndexes = zeroArray(palettes.length); + if (numColors > 2) { + const totalColorDistances = []; + for (let i = 0; i < palettes.length; i++) { + const totalColorDistance = zeroArray(numColors); + totalColorDistances.push(totalColorDistance); + } + for (const tile of tiles) { + const closestPaletteIndex = getClosestPaletteIndex(palettes, tile); + const palette = palettes[closestPaletteIndex]; + const colors = tile.colors; + const counts = tile.counts; + for (let i = 0; i < colors.length; i++) { + const [minIndex, minDist] = getClosestColor(palette, colors[i]); + totalColorDistances[closestPaletteIndex][minIndex] += + counts[i] * minDist; + } + } + for (let i = 0; i < palettes.length; i++) { + splitIndexes[i] = maxIndex(totalColorDistances[i]); + } + } + for (let i = 0; i < palettes.length; i++) { + const colors = palettes[i]; + const splitIndex = splitIndexes[i]; + colors.push(cloneColor(colors[splitIndex])); + } + for (let iteration = 0; iteration < iterations; iteration++) { + const nextPixel = pixels[randomShuffle.next()]; + movePalettesCloser(palettes, nextPixel, alpha); + } +} +function colorQuantize1Palette(pixels, randomShuffle, colorsPerPalette) { + let iterations = quantizationOptions.fractionOfPixels * pixels.length; + if (quantizationOptions.dither === Dither.Slow) { + iterations /= 5; + } + const errorStartIteration = iterations * 0.5; + const alpha = 0.3; + const colorZeroBehaviour = quantizationOptions.colorZeroBehaviour; + if (colorZeroBehaviour === ColorZeroBehaviour.TransparentFromColor || + colorZeroBehaviour === ColorZeroBehaviour.TransparentFromTransparent) { + colorsPerPalette -= 1; + } + // find average color + const avgColor = [0, 0, 0]; + for (const pixel of pixels) { + addColor(avgColor, pixel.color); + } + scaleColor(avgColor, 1.0 / pixels.length); + let sharedColorIndex = -1; + if (colorZeroBehaviour === ColorZeroBehaviour.Shared) { + sharedColorIndex = 0; + } + const colors = [avgColor]; + let splitIndex = 0; + for (let numColors = 2; numColors <= colorsPerPalette; numColors++) { + if (numColors === 2 && + colorZeroBehaviour === ColorZeroBehaviour.Shared) { + colors[0] = cloneColor(quantizationOptions.colorZeroValue); + colors.push(avgColor); + } + else { + colors.push(cloneColor(colors[splitIndex])); + } + const totalColorDistance = new Array(numColors); + for (let i = 0; i < numColors; i++) { + totalColorDistance[i] = 0.0; + } + for (let iteration = 0; iteration < iterations; iteration++) { + const nextPixel = pixels[randomShuffle.next()]; + let minColorIndex = -1; + let minColorDistance = -1; + let targetColor; + if (quantizationOptions.dither === Dither.Slow) { + [minColorIndex, minColorDistance, targetColor] = + getClosestColorDither(colors, nextPixel); + } + else { + [minColorIndex, minColorDistance] = getClosestColor(colors, nextPixel.color); + targetColor = nextPixel.color; + } + if (minColorIndex !== sharedColorIndex) { + moveColorCloser(colors[minColorIndex], targetColor, alpha); + } + if (iteration > errorStartIteration) { + totalColorDistance[minColorIndex] += minColorDistance; + } + } + splitIndex = maxIndex(totalColorDistance); + } + return colors; +} +function cloneColor(color) { + const result = [0, 0, 0]; + for (let i = 0; i < 3; i++) { + result[i] = color[i]; + } + return result; +} +function copyColor(dest, source) { + for (let i = 0; i < 3; i++) { + dest[i] = source[i]; + } +} +function addColor(c1, c2) { + for (let i = 0; i < 3; i++) { + c1[i] += c2[i]; + } +} +function subtractColor(c1, c2) { + for (let i = 0; i < 3; i++) { + c1[i] -= c2[i]; + } +} +function scaleColor(color, scaleFactor) { + for (let i = 0; i < 3; i++) { + color[i] *= scaleFactor; + } +} +function clampColor(color, minValue, maxValue) { + for (let i = 0; i < 3; i++) { + if (color[i] < minValue) { + color[i] = minValue; + } + else if (color[i] > maxValue) { + color[i] = maxValue; + } + } +} +// alpha = 255 / (2 ** n - 1) +const alphaValues = [0, 255, 85, 36.42857, 17, 8.22581, 4.04762, 2.00787, 1]; +function toNbit(value, n) { + const alpha = alphaValues[n]; + return Math.round(Math.round(value / alpha) * alpha); +} +function toNbitColor(color, n) { + for (let i = 0; i < 3; i++) { + color[i] = toNbit(color[i], n); + } +} +function moveColorCloser(color, pixelColor, alpha) { + for (let i = 0; i < color.length; i++) { + color[i] = (1 - alpha) * color[i] + alpha * pixelColor[i]; + } +} +function maxIndex(values) { + let maxI = 0; + for (let i = 1; i < values.length; i++) { + if (values[i] > values[maxI]) { + maxI = i; + } + } + return maxI; +} +function minIndex(values) { + let minI = 0; + for (let i = 1; i < values.length; i++) { + if (values[i] < values[minI]) { + minI = i; + } + } + return minI; +} +//# sourceMappingURL=worker.js.map \ No newline at end of file From 06dd119ea05bf6a591ee3d6f05604edce5dc49ad Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Thu, 9 May 2024 16:03:06 +0200 Subject: [PATCH 057/106] chore(*): add explanations with examples --- wiki/Sounds-and-Musics.md | 108 +++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/wiki/Sounds-and-Musics.md b/wiki/Sounds-and-Musics.md index 362bf07f..8b11ba02 100644 --- a/wiki/Sounds-and-Musics.md +++ b/wiki/Sounds-and-Musics.md @@ -1,4 +1,4 @@ -PVSneslib uses IT files to play musics, it also uses a specific IT file for sound effects. +PVSneslib uses Impulse Tracker (IT) files to play musics, it also uses a specific IT file for sound effects. ## Tools to create IT files @@ -60,4 +60,108 @@ however, I may have to make additional adjustments depending on how the loop poi - Note Cut is the only New Note Action supported for SNESMod. One of these songs is the most visibly affected by this problem, and that's because SNESMod doesn't virtually allocate channels. -You have to modify the patterns so that the note off commands go where the note would originally play, and the new note is put on another channel. \ No newline at end of file +You have to modify the patterns so that the note off commands go where the note would originally play, and the new note is put on another channel. + +## Adding music to your game + +To add music capabilty to your game, you must begin your main file with the boot initialization of the SPC700 processor. +This **spcBoot** function will copy the Impulse Tracker emulator to SPC700. +You will find all audio examples in **snes-examples/audio**, shipped with PVSneslib release. + +```bash +#include + +//--------------------------------------------------------------------------------- +int main(void) +{ + // Initialize sound engine (take some time) + spcBoot(); +``` + +### Compiling musics and initialize the banks used by musics + +To add musics to your game, you must compile them to allow the driver to play them in the game. +**smconv** is the tool shipped with PVSneslib to do the job. + +```bash +Put your IT files in a res subdirectory of your game, and add them to your makefile. +# list in AUDIOFILES all your .it files in the right order. It will build to generate soundbank file +AUDIOFILES := res/whatislove.it +# then define the path to generate soundbank data. The name can be different but do not forget to update your include in .c file ! +export SOUNDBANK := res/soundbank +``` + +Add the correct parameters to your makefile conditions. + +```bash +# to build musics, define SMCONVFLAGS with parameters you want +SMCONVFLAGS := -s -o $(SOUNDBANK) -V -b 5 +musics: $(SOUNDBANK).obj + +all: musics $(ROMNAME).sfc +``` + +We must initialize the sound banks containing the musics, in reverse order if you have more that one 32K bank. + +```bash +// soundbank that are declared in soundbank.asm +extern char SOUNDBANK__0, SOUNDBANK__1; +``` + +Later in your code, after the sound engine initialization, you must declare the sounbanks, in **reverse order**! + +```bash + // Set soundbank available in soundbank.asm. Yes, in reverse order ! + spcSetBank(&SOUNDBANK__1); + spcSetBank(&SOUNDBANK__0); +``` + +Check soundbank.asm file to know exactly how many banks you have. + +```bash +.BANK 5 +.SECTION "SOUNDBANK0" ; need dedicated bank(s) + +SOUNDBANK__0: +.incbin "res/soundbank.bnk" read $8000 +.ENDS + +.BANK 6 +.SECTION "SOUNDBANK1" ; need dedicated bank(s) + +SOUNDBANK__1: +.incbin "res/soundbank.bnk" skip $8000 +.ENDS +``` + +### Playing musics + +To play a music file, you must first load the music file and then play it. You must pay attention that it will take some time to load it. +It is because the SNES cpu will send the music to the audio CPU. + +```bash + // Load music + spcLoad(MOD_POLLEN8); +``` + +You play the file with a parameter, allowing you to begin the music when you want. 0 must be use to play it from the beginning. + +```bash + // Play file from the beginning + spcPlay(0); +``` +Use **spcStop()** to stop music. + +In your main loop, you must add a function named **spcProcess** just before waiting VBlank to allow the driver to process the music, without this instruction, it will not play music! + +```bash + // Update music / sfx stream and wait vbl + spcProcess(); + WaitForVBlank(); +``` + +## Adding sound effects to your game with some brr files + + +## Adding sound effects to your game with one IT file + From b0de8a7c512b8eb6acdd95b784ace398b060022d Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Thu, 9 May 2024 16:09:03 +0200 Subject: [PATCH 058/106] chore(*): no more need of consoleInit (done in crt0) --- snes-examples/audio/effects/effects.c | 3 --- .../audio/effectsandmusic/effectsandmusic.c | 3 --- snes-examples/audio/music/music.c | 4 ---- .../musicGreaterThan32k/musicGreaterThan32k.c | 3 --- snes-examples/audio/musicHiROM/musicHiROM.c | 3 --- snes-examples/audio/tada/tada.c | 3 --- snes-examples/breakpoints/src/breakpoints.c | 1 - snes-examples/debug/debug.c | 3 --- snes-examples/games/breakout/breakout.c | 3 --- snes-examples/games/likemario/LikeMario.c | 3 --- snes-examples/graphics/Backgrounds/Mode0/Mode0.c | 3 --- snes-examples/graphics/Backgrounds/Mode1/Mode1.c | 3 --- .../Mode1BG3HighPriority/Mode1BG3HighPriority.c | 4 ---- .../Mode1ContinuosScroll/Mode1ContinuosScroll.c | 3 --- .../graphics/Backgrounds/Mode1LZ77/Mode1LZ77.c | 7 ++----- .../Mode1MixedScroll/Mode1MixedScroll.c | 9 +++------ .../graphics/Backgrounds/Mode1Png/Mode1.c | 3 --- .../graphics/Backgrounds/Mode1Scroll/Mode1Scroll.c | 3 --- snes-examples/graphics/Backgrounds/Mode3/Mode3.c | 3 --- snes-examples/graphics/Backgrounds/Mode5/Mode5.c | 3 --- snes-examples/graphics/Backgrounds/Mode7/Mode7.c | 3 --- .../Mode7Perspective/Mode7Perspective.c | 3 --- snes-examples/graphics/Effects/Fading/Fading.c | 3 --- .../Effects/GradientColors/GradientColors.c | 3 --- .../graphics/Effects/HDMAGradient/HDMAGradient.c | 3 --- .../graphics/Effects/MosaicShading/MosaicShading.c | 3 --- .../Effects/ParallaxScrolling/ParallaxScrolling.c | 3 --- .../graphics/Effects/Transparency/Transparency.c | 9 +++------ .../graphics/Effects/TransparentWindow/src/main.c | 2 -- snes-examples/graphics/Effects/Waves/Waves.c | 3 --- snes-examples/graphics/Effects/Window/Window.c | 3 --- .../graphics/Palette/GetColors/GetColors.c | 3 --- .../Sprites/AnimatedSprite/AnimatedSprite.c | 14 ++++++-------- .../DynamicEngineMetaSprite.c | 3 --- .../DynamicEngineSprite/DynamicEngineSprite.c | 3 --- .../graphics/Sprites/DynamicSprite/DynamicSprite.c | 3 --- .../graphics/Sprites/ObjectSize/ObjectSize.c | 6 +----- .../graphics/Sprites/SimpleSprite/SimpleSprite.c | 3 --- snes-examples/hello_world/src/hello_world.c | 3 --- snes-examples/input/controller/controller.c | 7 ++----- snes-examples/input/mouse/mouse.c | 3 --- snes-examples/input/multiplay5/multiplay5.c | 6 +----- snes-examples/input/superscope/superscope.c | 2 -- snes-examples/logo/snes-logo-capcom/src/main.c | 3 --- snes-examples/logo/snes-logo-konami/src/main.c | 3 --- snes-examples/logo/snes-logo-pvsneslib/src/main.c | 3 --- snes-examples/maps/mapscroll/mapscroll.c | 3 --- snes-examples/maps/slopemario/slopemario.c | 3 --- snes-examples/maps/tiled/tiled.c | 3 --- snes-examples/memory_mapping/src/memory_mapping.c | 3 --- .../objects/mapandobjects/mapandobjects.c | 3 --- snes-examples/objects/moveobjects/moveobjects.c | 3 --- .../objects/nogravityobject/nogravityobjects.c | 3 --- snes-examples/random/random.c | 3 --- snes-examples/scoring/scoring.c | 3 --- snes-examples/sram/sramoffset/sramoffset.c | 8 +++----- snes-examples/sram/sramsimple/sram.c | 9 +++------ snes-examples/testregion/testregion.c | 3 --- snes-examples/timer/timer.c | 3 --- snes-examples/typeconsole/src/pal_ntsc.c | 3 --- 60 files changed, 24 insertions(+), 202 deletions(-) diff --git a/snes-examples/audio/effects/effects.c b/snes-examples/audio/effects/effects.c index dd7ea1ea..2404b61b 100644 --- a/snes-examples/audio/effects/effects.c +++ b/snes-examples/audio/effects/effects.c @@ -26,9 +26,6 @@ int main(void) // Initialize sound engine (take some time) spcBoot(); - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/audio/effectsandmusic/effectsandmusic.c b/snes-examples/audio/effectsandmusic/effectsandmusic.c index 8288a13c..4e9f5b98 100644 --- a/snes-examples/audio/effectsandmusic/effectsandmusic.c +++ b/snes-examples/audio/effectsandmusic/effectsandmusic.c @@ -26,9 +26,6 @@ int main(void) // Initialize sound engine (take some time) spcBoot(); - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/audio/music/music.c b/snes-examples/audio/music/music.c index aa23a643..4190239d 100644 --- a/snes-examples/audio/music/music.c +++ b/snes-examples/audio/music/music.c @@ -18,13 +18,9 @@ unsigned short bgcolor = 0; //--------------------------------------------------------------------------------- int main(void) { - // Initialize sound engine (take some time) spcBoot(); - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/audio/musicGreaterThan32k/musicGreaterThan32k.c b/snes-examples/audio/musicGreaterThan32k/musicGreaterThan32k.c index 863a4a4a..11d4964b 100644 --- a/snes-examples/audio/musicGreaterThan32k/musicGreaterThan32k.c +++ b/snes-examples/audio/musicGreaterThan32k/musicGreaterThan32k.c @@ -19,9 +19,6 @@ int main(void) // Initialize sound engine (take some time) spcBoot(); - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/audio/musicHiROM/musicHiROM.c b/snes-examples/audio/musicHiROM/musicHiROM.c index ec0c74f5..5678b4db 100644 --- a/snes-examples/audio/musicHiROM/musicHiROM.c +++ b/snes-examples/audio/musicHiROM/musicHiROM.c @@ -40,9 +40,6 @@ int main(void) // Initialize sound engine (take some time) spcBoot(); - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/audio/tada/tada.c b/snes-examples/audio/tada/tada.c index 657605b5..23b750e0 100644 --- a/snes-examples/audio/tada/tada.c +++ b/snes-examples/audio/tada/tada.c @@ -23,9 +23,6 @@ int main(void) // Initialize sound engine (take some time) spcBoot(); - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/breakpoints/src/breakpoints.c b/snes-examples/breakpoints/src/breakpoints.c index 0608252f..5d4f1832 100644 --- a/snes-examples/breakpoints/src/breakpoints.c +++ b/snes-examples/breakpoints/src/breakpoints.c @@ -18,7 +18,6 @@ int main(void) { // Initialize SNES consoleMesenBreakpoint(); - consoleInit(); // Initialize text console with our font consoleMesenBreakpoint(); diff --git a/snes-examples/debug/debug.c b/snes-examples/debug/debug.c index 9b1dc1c8..6db42375 100644 --- a/snes-examples/debug/debug.c +++ b/snes-examples/debug/debug.c @@ -11,9 +11,6 @@ //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Draw a wonderful text :P consoleNocashMessage("JUST COUNT VBL\r"); diff --git a/snes-examples/games/breakout/breakout.c b/snes-examples/games/breakout/breakout.c index a9a70727..ec9c47c7 100644 --- a/snes-examples/games/breakout/breakout.c +++ b/snes-examples/games/breakout/breakout.c @@ -506,9 +506,6 @@ void run_frame(void) //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Put screen off and Wait VBlank to allow us to update VRAM setBrightness(0); WaitForVBlank(); diff --git a/snes-examples/games/likemario/LikeMario.c b/snes-examples/games/likemario/LikeMario.c index 06cd6142..99080f3f 100644 --- a/snes-examples/games/likemario/LikeMario.c +++ b/snes-examples/games/likemario/LikeMario.c @@ -238,9 +238,6 @@ int main(void) // Initialize sound engine (take some time) spcBoot(); - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6000); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/graphics/Backgrounds/Mode0/Mode0.c b/snes-examples/graphics/Backgrounds/Mode0/Mode0.c index 6b3cd30b..9d7eefed 100644 --- a/snes-examples/graphics/Backgrounds/Mode0/Mode0.c +++ b/snes-examples/graphics/Backgrounds/Mode0/Mode0.c @@ -18,9 +18,6 @@ short sxbg1 = 0, sxbg2 = 0, sxbg3 = 0, flip = 0; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Copy tiles to VRAM, begin with adr=0x2000 for first BG // ALso, always use specific mode0 palette entry (ppp) // BG1 w/bits ppp=0 is entries 0-3 diff --git a/snes-examples/graphics/Backgrounds/Mode1/Mode1.c b/snes-examples/graphics/Backgrounds/Mode1/Mode1.c index 39dbd458..a744a8b4 100644 --- a/snes-examples/graphics/Backgrounds/Mode1/Mode1.c +++ b/snes-examples/graphics/Backgrounds/Mode1/Mode1.c @@ -15,9 +15,6 @@ extern char map, map_end; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Copy tiles to VRAM bgInitTileSet(0, &patterns, &palette, 0, (&patterns_end - &patterns), (&palette_end - &palette), BG_16COLORS, 0x4000); diff --git a/snes-examples/graphics/Backgrounds/Mode1BG3HighPriority/Mode1BG3HighPriority.c b/snes-examples/graphics/Backgrounds/Mode1BG3HighPriority/Mode1BG3HighPriority.c index b70fb753..1c65e54a 100644 --- a/snes-examples/graphics/Backgrounds/Mode1BG3HighPriority/Mode1BG3HighPriority.c +++ b/snes-examples/graphics/Backgrounds/Mode1BG3HighPriority/Mode1BG3HighPriority.c @@ -26,10 +26,6 @@ extern char BG3_map, BG3_map_end; //--------------------------------------------------------------------------------- int main(void) { - - // Initialize SNES - consoleInit(); - // initializing background map tiles // BG0 point to vram address 0x0000 and with one pages of 32 tiles (32x32) diff --git a/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c b/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c index 9c790014..2a85d2a9 100644 --- a/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c +++ b/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c @@ -209,9 +209,6 @@ void myconsoleVblank() int main(void) { - // Initialize SNES - consoleInit(); - // initializing background map tiles // BG0 point to vram address 0x0000 and with two pages of 32 tiles (64x32) diff --git a/snes-examples/graphics/Backgrounds/Mode1LZ77/Mode1LZ77.c b/snes-examples/graphics/Backgrounds/Mode1LZ77/Mode1LZ77.c index 1cf64676..5ebde178 100644 --- a/snes-examples/graphics/Backgrounds/Mode1LZ77/Mode1LZ77.c +++ b/snes-examples/graphics/Backgrounds/Mode1LZ77/Mode1LZ77.c @@ -2,8 +2,8 @@ Simple tile mode 1 demo with png graphic lzss compressed - Without compression : 12,2 Ko (12 544 octets) - With compression : 8,48 Ko (8 687 octets) + Without compression : 12,2 Ko (12�544 octets) + With compression : 8,48 Ko (8�687 octets) -- alekmaul @@ -16,9 +16,6 @@ extern char map, map_end; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Copy tiles to VRAM bgInitTileSetLz(0, &patterns, &palette, 0, (&palette_end - &palette), BG_16COLORS, 0x4000); diff --git a/snes-examples/graphics/Backgrounds/Mode1MixedScroll/Mode1MixedScroll.c b/snes-examples/graphics/Backgrounds/Mode1MixedScroll/Mode1MixedScroll.c index 73ea8aea..03d8869c 100644 --- a/snes-examples/graphics/Backgrounds/Mode1MixedScroll/Mode1MixedScroll.c +++ b/snes-examples/graphics/Backgrounds/Mode1MixedScroll/Mode1MixedScroll.c @@ -14,15 +14,12 @@ extern char palpvsneslib, palpvsneslib_end, palshader, palshader_end; extern char mappvsneslib, mappvsneslib_end; extern char mapshader, mapshader_end; +short scrX = 0, scrY = 0; +unsigned short pad0, move; + //--------------------------------------------------------------------------------- int main(void) { - short scrX = 0, scrY = 0; - unsigned short pad0, move; - - // Initialize SNES - consoleInit(); - // Copy tiles to VRAM bgInitTileSet(1, &patpvsneslib, &palpvsneslib, 0, (&patpvsneslib_end - &patpvsneslib), 16 * 2, BG_16COLORS, 0x5000); bgInitTileSet(0, &patshader, &palshader, 1, (&patshader_end - &patshader), 16 * 2, BG_16COLORS, 0x4000); diff --git a/snes-examples/graphics/Backgrounds/Mode1Png/Mode1.c b/snes-examples/graphics/Backgrounds/Mode1Png/Mode1.c index 0a5b0ea0..d28e276d 100644 --- a/snes-examples/graphics/Backgrounds/Mode1Png/Mode1.c +++ b/snes-examples/graphics/Backgrounds/Mode1Png/Mode1.c @@ -15,9 +15,6 @@ extern char map, map_end; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Copy tiles to VRAM bgInitTileSet(0, &patterns, &palette, 0, (&patterns_end - &patterns), (&palette_end - &palette), BG_16COLORS, 0x4000); diff --git a/snes-examples/graphics/Backgrounds/Mode1Scroll/Mode1Scroll.c b/snes-examples/graphics/Backgrounds/Mode1Scroll/Mode1Scroll.c index bf83251d..7be49a56 100644 --- a/snes-examples/graphics/Backgrounds/Mode1Scroll/Mode1Scroll.c +++ b/snes-examples/graphics/Backgrounds/Mode1Scroll/Mode1Scroll.c @@ -20,9 +20,6 @@ int main(void) u16 scrX = 0, scrY = 0; u16 pad0, move; - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/graphics/Backgrounds/Mode3/Mode3.c b/snes-examples/graphics/Backgrounds/Mode3/Mode3.c index 99093880..d5e4f63b 100644 --- a/snes-examples/graphics/Backgrounds/Mode3/Mode3.c +++ b/snes-examples/graphics/Backgrounds/Mode3/Mode3.c @@ -15,9 +15,6 @@ extern char map, map_end; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Read tiles to VRAM in 2 phases because we are more than 32k bgInitTileSet(0, &patterns, &palette, 0, 0x8000, 256 * 2, BG_256COLORS, 0x0000); WaitForVBlank(); diff --git a/snes-examples/graphics/Backgrounds/Mode5/Mode5.c b/snes-examples/graphics/Backgrounds/Mode5/Mode5.c index 1f5e1656..df28c2d8 100644 --- a/snes-examples/graphics/Backgrounds/Mode5/Mode5.c +++ b/snes-examples/graphics/Backgrounds/Mode5/Mode5.c @@ -14,9 +14,6 @@ extern char map, map_end; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Copy tiles to VRAM bgInitTileSet(0, &patterns, &palette, 0, (&patterns_end - &patterns), (&palette_end - &palette), BG_16COLORS, 0x0000); diff --git a/snes-examples/graphics/Backgrounds/Mode7/Mode7.c b/snes-examples/graphics/Backgrounds/Mode7/Mode7.c index 02559804..d86af4f4 100644 --- a/snes-examples/graphics/Backgrounds/Mode7/Mode7.c +++ b/snes-examples/graphics/Backgrounds/Mode7/Mode7.c @@ -18,9 +18,6 @@ u8 angle; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Read tiles & map to VRAM (interlace for mode 7) bgInitMapTileSet7(&patterns, &map, &palette, (&patterns_end - &patterns), 0x0000); diff --git a/snes-examples/graphics/Backgrounds/Mode7Perspective/Mode7Perspective.c b/snes-examples/graphics/Backgrounds/Mode7Perspective/Mode7Perspective.c index 914f6a53..2ecf9ddf 100644 --- a/snes-examples/graphics/Backgrounds/Mode7Perspective/Mode7Perspective.c +++ b/snes-examples/graphics/Backgrounds/Mode7Perspective/Mode7Perspective.c @@ -408,9 +408,6 @@ void setMode7_HdmaPerspective(void) //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // copy a mode 7 map and tiles (1024x1024 - VRAM 0000), to fake a ground bgInitMapTileSet7(&patterns, &map, &palette, (&patterns_end - &patterns), 0x0000); diff --git a/snes-examples/graphics/Effects/Fading/Fading.c b/snes-examples/graphics/Effects/Fading/Fading.c index 704d4a57..fc615489 100644 --- a/snes-examples/graphics/Effects/Fading/Fading.c +++ b/snes-examples/graphics/Effects/Fading/Fading.c @@ -15,9 +15,6 @@ extern char map, map_end; //--------------------------------------------------------------------------------- int main(void) { - // Initialize console - consoleInit(); - // Copy tiles to VRAM bgInitTileSet(0, &patterns, &palette, 0, (&patterns_end - &patterns), (&palette_end - &palette), BG_16COLORS, 0x4000); diff --git a/snes-examples/graphics/Effects/GradientColors/GradientColors.c b/snes-examples/graphics/Effects/GradientColors/GradientColors.c index 91096143..d8b1003f 100644 --- a/snes-examples/graphics/Effects/GradientColors/GradientColors.c +++ b/snes-examples/graphics/Effects/GradientColors/GradientColors.c @@ -20,9 +20,6 @@ u16 pad0; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Copy tiles to VRAM bgInitTileSet(0, &patterns, &palette, 0, (&patterns_end - &patterns), (&palette_end - &palette), BG_16COLORS, 0x4000); diff --git a/snes-examples/graphics/Effects/HDMAGradient/HDMAGradient.c b/snes-examples/graphics/Effects/HDMAGradient/HDMAGradient.c index 7e17a32c..db47a473 100644 --- a/snes-examples/graphics/Effects/HDMAGradient/HDMAGradient.c +++ b/snes-examples/graphics/Effects/HDMAGradient/HDMAGradient.c @@ -18,9 +18,6 @@ int main(void) unsigned short pad0, pad1 = 0; unsigned char gradient = 15; - // Initialize SNES - consoleInit(); - // Read tiles to VRAM in 2 phases because we are more than 32k bgInitTileSet(0, &patterns, &palette, 0, 0x8000, 256 * 2, BG_256COLORS, 0x1000); dmaCopyVram(&patterns1, 0x5000, 0x6000); diff --git a/snes-examples/graphics/Effects/MosaicShading/MosaicShading.c b/snes-examples/graphics/Effects/MosaicShading/MosaicShading.c index 7464235a..43b5d07e 100644 --- a/snes-examples/graphics/Effects/MosaicShading/MosaicShading.c +++ b/snes-examples/graphics/Effects/MosaicShading/MosaicShading.c @@ -15,9 +15,6 @@ extern char map, map_end; //--------------------------------------------------------------------------------- int main(void) { - // Initialize console - consoleInit(); - // Copy tiles to VRAM bgInitTileSet(0, &patterns, &palette, 0, (&patterns_end - &patterns), (&palette_end - &palette), BG_16COLORS, 0x4000); diff --git a/snes-examples/graphics/Effects/ParallaxScrolling/ParallaxScrolling.c b/snes-examples/graphics/Effects/ParallaxScrolling/ParallaxScrolling.c index 70663b3f..bd889c71 100644 --- a/snes-examples/graphics/Effects/ParallaxScrolling/ParallaxScrolling.c +++ b/snes-examples/graphics/Effects/ParallaxScrolling/ParallaxScrolling.c @@ -15,9 +15,6 @@ extern char map; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Read tiles to VRAM bgInitTileSet(0, &back, &palette, 0, (&back_end - &back), 16 * 2 * 2, BG_16COLORS, 0x1000); diff --git a/snes-examples/graphics/Effects/Transparency/Transparency.c b/snes-examples/graphics/Effects/Transparency/Transparency.c index 8e186077..924007b8 100644 --- a/snes-examples/graphics/Effects/Transparency/Transparency.c +++ b/snes-examples/graphics/Effects/Transparency/Transparency.c @@ -14,15 +14,12 @@ extern char LandPalette, LandPalette_end, CloudPalette, CloudPalette_end; extern char Maps, Maps_end; extern char Mapsc, Mapsc_end; +short scrX = 0, scrY = 0; +unsigned short pad0, move; + //--------------------------------------------------------------------------------- int main(void) { - short scrX = 0, scrY = 0; - unsigned short pad0, move; - - // Initialize SNES - consoleInit(); - // Copy tiles to VRAM bgInitTileSet(0, &LandTiles, &LandPalette, 1, (&LandTiles_end - &LandTiles), (&LandPalette_end - &LandPalette), BG_16COLORS, 0x0000); bgInitTileSet(2, &CloudTiles, &CloudPalette, 0, (&CloudTiles_end - &CloudTiles), (&CloudPalette_end - &CloudPalette), BG_4COLORS, 0x1000); diff --git a/snes-examples/graphics/Effects/TransparentWindow/src/main.c b/snes-examples/graphics/Effects/TransparentWindow/src/main.c index 04919231..49508b62 100644 --- a/snes-examples/graphics/Effects/TransparentWindow/src/main.c +++ b/snes-examples/graphics/Effects/TransparentWindow/src/main.c @@ -30,8 +30,6 @@ extern char backgroundMap, backgroundMap_end; #define PAL7 7 int main(void) { - consoleInit(); - bgInitTileSet(BG1, &backgroundPic, &backgroundPalette, diff --git a/snes-examples/graphics/Effects/Waves/Waves.c b/snes-examples/graphics/Effects/Waves/Waves.c index cf57b805..f8df495b 100644 --- a/snes-examples/graphics/Effects/Waves/Waves.c +++ b/snes-examples/graphics/Effects/Waves/Waves.c @@ -18,9 +18,6 @@ u16 pad0; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Copy tiles to VRAM bgInitTileSet(0, &patterns, &palette, 0, (&patterns_end - &patterns), (&palette_end - &palette), BG_16COLORS, 0x4000); diff --git a/snes-examples/graphics/Effects/Window/Window.c b/snes-examples/graphics/Effects/Window/Window.c index 44badd18..f9f80140 100644 --- a/snes-examples/graphics/Effects/Window/Window.c +++ b/snes-examples/graphics/Effects/Window/Window.c @@ -51,9 +51,6 @@ const u8 tablerighttriangle[]= //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Copy tiles to VRAM bgInitTileSet(0, &patternsbg1, &palettebg1, 0, (&patternsbg1_end - &patternsbg1), 16*1*2, BG_16COLORS, 0x4000); bgInitTileSet(1, &patternsbg2, &palettebg2, 1, (&patternsbg2_end - &patternsbg2), 16*1*2, BG_16COLORS, 0x6000); diff --git a/snes-examples/graphics/Palette/GetColors/GetColors.c b/snes-examples/graphics/Palette/GetColors/GetColors.c index 8eda58be..53036cd0 100644 --- a/snes-examples/graphics/Palette/GetColors/GetColors.c +++ b/snes-examples/graphics/Palette/GetColors/GetColors.c @@ -22,9 +22,6 @@ u16 arraypals[3]; // array of colors for test //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/graphics/Sprites/AnimatedSprite/AnimatedSprite.c b/snes-examples/graphics/Sprites/AnimatedSprite/AnimatedSprite.c index 2e7d240e..c33ed115 100644 --- a/snes-examples/graphics/Sprites/AnimatedSprite/AnimatedSprite.c +++ b/snes-examples/graphics/Sprites/AnimatedSprite/AnimatedSprite.c @@ -49,18 +49,16 @@ enum }; const char sprTiles[9] = - { - 0, 2, 4, 6, 8, 10, 12, 14, 32}; // Remember that sprites are interleave with 128 pix width, +{ + 0, 2, 4, 6, 8, 10, 12, 14, 32 +}; // Remember that sprites are interleave with 128 pix width, + +unsigned short pad0, i; +Monster monster = {100, 100}; //--------------------------------------------------------------------------------- int main(void) { - unsigned short pad0, i; - Monster monster = {100, 100}; - - // Initialize SNES - consoleInit(); - // Init Sprites gfx and palette with default size of 16x16 oamInitGfxSet(&gfxpsrite, (&gfxpsrite_end - &gfxpsrite), &palsprite, (&palsprite_end - &palsprite), 0, 0x0000, OBJ_SIZE16_L32); diff --git a/snes-examples/graphics/Sprites/DynamicEngineMetaSprite/DynamicEngineMetaSprite.c b/snes-examples/graphics/Sprites/DynamicEngineMetaSprite/DynamicEngineMetaSprite.c index 937e4b3c..f832f0d2 100644 --- a/snes-examples/graphics/Sprites/DynamicEngineMetaSprite/DynamicEngineMetaSprite.c +++ b/snes-examples/graphics/Sprites/DynamicEngineMetaSprite/DynamicEngineMetaSprite.c @@ -25,9 +25,6 @@ const t_metasprites metasprite[] = //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Copy tiles to VRAM bgInitTileSet(0, &patterns, &palette, 0, (&patterns_end - &patterns), (&palette_end - &palette), BG_16COLORS, 0x4000); diff --git a/snes-examples/graphics/Sprites/DynamicEngineSprite/DynamicEngineSprite.c b/snes-examples/graphics/Sprites/DynamicEngineSprite/DynamicEngineSprite.c index 9a859fa9..011fe6bf 100644 --- a/snes-examples/graphics/Sprites/DynamicEngineSprite/DynamicEngineSprite.c +++ b/snes-examples/graphics/Sprites/DynamicEngineSprite/DynamicEngineSprite.c @@ -22,9 +22,6 @@ u8 i; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Copy tiles to VRAM bgInitTileSet(0, &patterns, &palette, 0, (&patterns_end - &patterns), (&palette_end - &palette), BG_16COLORS, 0x4000); diff --git a/snes-examples/graphics/Sprites/DynamicSprite/DynamicSprite.c b/snes-examples/graphics/Sprites/DynamicSprite/DynamicSprite.c index 61b60fc5..2f3c29c1 100644 --- a/snes-examples/graphics/Sprites/DynamicSprite/DynamicSprite.c +++ b/snes-examples/graphics/Sprites/DynamicSprite/DynamicSprite.c @@ -70,9 +70,6 @@ void addSprite(u8 *pgfx, u16 adrspr) //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Put current handler to our function spr_queue = 0xff; spr_mutex = 0; diff --git a/snes-examples/graphics/Sprites/ObjectSize/ObjectSize.c b/snes-examples/graphics/Sprites/ObjectSize/ObjectSize.c index e3546204..a4a772a3 100644 --- a/snes-examples/graphics/Sprites/ObjectSize/ObjectSize.c +++ b/snes-examples/graphics/Sprites/ObjectSize/ObjectSize.c @@ -15,6 +15,7 @@ extern char sprite64, sprite64_end, palsprite64, palsprite64_end; u16 selectedItem; bool keyPressed; +unsigned short pad0; #define ADRBG1 0x2000 @@ -103,11 +104,6 @@ void changeObjSize() //--------------------------------------------------------------------------------- int main(void) { - unsigned short pad0; - - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/graphics/Sprites/SimpleSprite/SimpleSprite.c b/snes-examples/graphics/Sprites/SimpleSprite/SimpleSprite.c index 687d790b..902a3090 100644 --- a/snes-examples/graphics/Sprites/SimpleSprite/SimpleSprite.c +++ b/snes-examples/graphics/Sprites/SimpleSprite/SimpleSprite.c @@ -14,9 +14,6 @@ extern char palsprite, palsprite_end; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Init Sprites gfx and palette with default size of 32x32 oamInitGfxSet(&gfxpsrite, (&gfxpsrite_end - &gfxpsrite), &palsprite, (&palsprite_end - &palsprite), 0, 0x0000, OBJ_SIZE32_L64); diff --git a/snes-examples/hello_world/src/hello_world.c b/snes-examples/hello_world/src/hello_world.c index f558a309..2224d7f4 100644 --- a/snes-examples/hello_world/src/hello_world.c +++ b/snes-examples/hello_world/src/hello_world.c @@ -13,9 +13,6 @@ extern char tilfont, palfont; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/input/controller/controller.c b/snes-examples/input/controller/controller.c index 3068c353..c4277424 100644 --- a/snes-examples/input/controller/controller.c +++ b/snes-examples/input/controller/controller.c @@ -10,14 +10,11 @@ extern char snesfont, snespal; +unsigned short pad0; + //--------------------------------------------------------------------------------- int main(void) { - unsigned short pad0; - - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/input/mouse/mouse.c b/snes-examples/input/mouse/mouse.c index 8329857d..01b0b69b 100644 --- a/snes-examples/input/mouse/mouse.c +++ b/snes-examples/input/mouse/mouse.c @@ -40,9 +40,6 @@ bool speedset[2] = {true}; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Init cursors sprite oamInitGfxSet(&cursorsprite, (&cursorsprite_end - &cursorsprite), &cursorpal, 48 * 2, 0, 0x0000, OBJ_SIZE16_L32); diff --git a/snes-examples/input/multiplay5/multiplay5.c b/snes-examples/input/multiplay5/multiplay5.c index fc17b86c..c28458ca 100644 --- a/snes-examples/input/multiplay5/multiplay5.c +++ b/snes-examples/input/multiplay5/multiplay5.c @@ -11,15 +11,11 @@ extern char snesfont, snespal; u8 i; +unsigned short pad0; //--------------------------------------------------------------------------------- int main(void) { - unsigned short pad0; - - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/input/superscope/superscope.c b/snes-examples/input/superscope/superscope.c index c76b4b99..88050843 100644 --- a/snes-examples/input/superscope/superscope.c +++ b/snes-examples/input/superscope/superscope.c @@ -66,8 +66,6 @@ void resetGame() // Main program int main(void) { - consoleInit(); // Initialize SNES - // Initialize text console with our font consoleSetTextVramBGAdr(0x2000); consoleSetTextVramAdr(0x3800); diff --git a/snes-examples/logo/snes-logo-capcom/src/main.c b/snes-examples/logo/snes-logo-capcom/src/main.c index 771f87a8..5f41d81f 100644 --- a/snes-examples/logo/snes-logo-capcom/src/main.c +++ b/snes-examples/logo/snes-logo-capcom/src/main.c @@ -13,9 +13,6 @@ int main(void) { // Initialize sound engine (take some time) spcBoot(); - // Initialize SNES - consoleInit(); - dmaClearVram(); initCapcomLogo(); diff --git a/snes-examples/logo/snes-logo-konami/src/main.c b/snes-examples/logo/snes-logo-konami/src/main.c index 6ff7d9cf..c29c051f 100644 --- a/snes-examples/logo/snes-logo-konami/src/main.c +++ b/snes-examples/logo/snes-logo-konami/src/main.c @@ -13,9 +13,6 @@ int main(void) { // Initialize sound engine (take some time) spcBoot(); - // Initialize SNES - consoleInit(); - dmaClearVram(); initKonamiLogo(); diff --git a/snes-examples/logo/snes-logo-pvsneslib/src/main.c b/snes-examples/logo/snes-logo-pvsneslib/src/main.c index 34d30d9f..ffa3df40 100644 --- a/snes-examples/logo/snes-logo-pvsneslib/src/main.c +++ b/snes-examples/logo/snes-logo-pvsneslib/src/main.c @@ -12,9 +12,6 @@ int main(void) { // Initialize sound engine (take some time) spcBoot(); - // Initialize SNES - consoleInit(); - dmaClearVram(); initPVSnesLibLogo(); diff --git a/snes-examples/maps/mapscroll/mapscroll.c b/snes-examples/maps/mapscroll/mapscroll.c index de8a9e92..2f8fad86 100644 --- a/snes-examples/maps/mapscroll/mapscroll.c +++ b/snes-examples/maps/mapscroll/mapscroll.c @@ -28,9 +28,6 @@ unsigned short xloc, yloc, flipx, frame, frameidx, flip; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Init layer with tiles and init also map length 0x6800 is mandatory for map engine bgInitTileSet(0, &tileset, &tilesetpal, 0, (&tilesetend - &tileset), 16 * 2, BG_16COLORS, 0x2000); bgSetMapPtr(0, 0x6800, SC_64x32); diff --git a/snes-examples/maps/slopemario/slopemario.c b/snes-examples/maps/slopemario/slopemario.c index d692fbb8..9e678b13 100644 --- a/snes-examples/maps/slopemario/slopemario.c +++ b/snes-examples/maps/slopemario/slopemario.c @@ -242,10 +242,7 @@ void marioupdate(u8 idx) int main(void) { // Initialize sound engine (take some time) - spcBoot(); - // Initialize SNES - consoleInit(); // Initialize text console with our font consoleSetTextVramBGAdr(0x6000); diff --git a/snes-examples/maps/tiled/tiled.c b/snes-examples/maps/tiled/tiled.c index 757f91b7..1108a271 100644 --- a/snes-examples/maps/tiled/tiled.c +++ b/snes-examples/maps/tiled/tiled.c @@ -20,9 +20,6 @@ u8 keyl, keyr; // to update move //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Init layer with tiles and init also map length 0x6800 is mandatory for map engine bgInitTileSet(0, &tileset, &tilesetpal, 0, (&tilesetend - &tileset), 16 * 2 * 3, BG_16COLORS, 0x2000); bgSetMapPtr(0, 0x6800, SC_64x32); diff --git a/snes-examples/memory_mapping/src/memory_mapping.c b/snes-examples/memory_mapping/src/memory_mapping.c index a58a7834..babf2fd6 100644 --- a/snes-examples/memory_mapping/src/memory_mapping.c +++ b/snes-examples/memory_mapping/src/memory_mapping.c @@ -14,9 +14,6 @@ extern char tilfont, palfont; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/objects/mapandobjects/mapandobjects.c b/snes-examples/objects/mapandobjects/mapandobjects.c index ae202c23..c0d550e9 100644 --- a/snes-examples/objects/mapandobjects/mapandobjects.c +++ b/snes-examples/objects/mapandobjects/mapandobjects.c @@ -23,9 +23,6 @@ u16 nbobjects; // to init more than sprite object in map //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Init layer with tiles and init also map length 0x6800 is mandatory for map engine bgInitTileSet(0, &tileset, &tilesetpal, 0, (&tilesetend - &tileset), 16 * 2, BG_16COLORS, 0x2000); bgSetMapPtr(0, 0x6800, SC_64x32); diff --git a/snes-examples/objects/moveobjects/moveobjects.c b/snes-examples/objects/moveobjects/moveobjects.c index 739324ef..6d049f9f 100644 --- a/snes-examples/objects/moveobjects/moveobjects.c +++ b/snes-examples/objects/moveobjects/moveobjects.c @@ -151,9 +151,6 @@ void testUpdate(u8 idx) //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Init Sprites gfx and palette with default size of 32x32 oamInitGfxSet(&gfxpsrite, (&gfxpsrite_end - &gfxpsrite), &palsprite, (&palsprite_end - &palsprite), 0, 0x0000, OBJ_SIZE32_L64); diff --git a/snes-examples/objects/nogravityobject/nogravityobjects.c b/snes-examples/objects/nogravityobject/nogravityobjects.c index b718a42f..e4e97a36 100644 --- a/snes-examples/objects/nogravityobject/nogravityobjects.c +++ b/snes-examples/objects/nogravityobject/nogravityobjects.c @@ -21,9 +21,6 @@ u16 sprnum; // to manage update of each sprite on screen //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Init layer with tiles and init also map length 0x6800 is mandatory for map engine bgInitTileSet(0, &tileset, &tilepal, 0, (&tilesetend - &tileset), 16 * 2, BG_16COLORS, 0x2000); bgSetMapPtr(0, 0x6800, SC_64x32); diff --git a/snes-examples/random/random.c b/snes-examples/random/random.c index 72a1f2fb..6cb86f76 100644 --- a/snes-examples/random/random.c +++ b/snes-examples/random/random.c @@ -15,9 +15,6 @@ unsigned short pad0; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/scoring/scoring.c b/snes-examples/scoring/scoring.c index f9b7bacf..c9ba3130 100644 --- a/snes-examples/scoring/scoring.c +++ b/snes-examples/scoring/scoring.c @@ -16,9 +16,6 @@ char sz[128]; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/sram/sramoffset/sramoffset.c b/snes-examples/sram/sramoffset/sramoffset.c index 096a55e4..cb6da0a7 100644 --- a/snes-examples/sram/sramoffset/sramoffset.c +++ b/snes-examples/sram/sramoffset/sramoffset.c @@ -13,6 +13,9 @@ extern char snesfont, snespal; char sz[16]; + +unsigned short pad0; + typedef struct { s16 posX, posY; @@ -67,11 +70,6 @@ void LoadSlot2() int main(void) { - unsigned short pad0; - - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/sram/sramsimple/sram.c b/snes-examples/sram/sramsimple/sram.c index 5461bb9c..d0def8fc 100644 --- a/snes-examples/sram/sramsimple/sram.c +++ b/snes-examples/sram/sramsimple/sram.c @@ -12,15 +12,12 @@ extern char snesfont, snespal; unsigned short valToSave = 0xCAFE, valToLoad; +unsigned short pad0; +char sz[16]; + //--------------------------------------------------------------------------------- int main(void) { - unsigned short pad0; - char sz[16]; - - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/testregion/testregion.c b/snes-examples/testregion/testregion.c index 9b1f9570..e5998e17 100644 --- a/snes-examples/testregion/testregion.c +++ b/snes-examples/testregion/testregion.c @@ -16,9 +16,6 @@ unsigned short pad0; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/timer/timer.c b/snes-examples/timer/timer.c index 1ba47a91..1b22835f 100644 --- a/snes-examples/timer/timer.c +++ b/snes-examples/timer/timer.c @@ -13,9 +13,6 @@ extern char snesfont, snespal; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); diff --git a/snes-examples/typeconsole/src/pal_ntsc.c b/snes-examples/typeconsole/src/pal_ntsc.c index de8ac9d7..bab0128b 100644 --- a/snes-examples/typeconsole/src/pal_ntsc.c +++ b/snes-examples/typeconsole/src/pal_ntsc.c @@ -13,9 +13,6 @@ extern char snesfont, snespal; //--------------------------------------------------------------------------------- int main(void) { - // Initialize SNES - consoleInit(); - // Initialize text console with our font consoleSetTextVramBGAdr(0x6800); consoleSetTextVramAdr(0x3000); From 555a85b9cd5ad41662afeae405e47e3684486eb7 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Thu, 9 May 2024 16:09:52 +0200 Subject: [PATCH 059/106] chore(*): add few VBL code in crt0 and remove from default vblank --- pvsneslib/source/consoles.asm | 44 +++++++++------------------------ pvsneslib/source/crt0_snes.asm | 45 ++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/pvsneslib/source/consoles.asm b/pvsneslib/source/consoles.asm index 02921789..c2b3cd67 100644 --- a/pvsneslib/source/consoles.asm +++ b/pvsneslib/source/consoles.asm @@ -1,6 +1,6 @@ ;--------------------------------------------------------------------------------- ; -; Copyright (C) 2013-2023 +; Copyright (C) 2013-2024 ; Alekmaul ; ; This software is provided 'as-is', without any express or implied @@ -22,21 +22,21 @@ ; ;--------------------------------------------------------------------------------- -.EQU REG_CGADD $2121 -.EQU CGRAM_PALETTE $2122 +.EQU REG_CGADD $2121 +.EQU CGRAM_PALETTE $2122 -.EQU REG_STAT78 $213F -.EQU REG_DEBUG $21FC +.EQU REG_STAT78 $213F +.EQU REG_DEBUG $21FC -.EQU BANK_SRAM $70 -.EQU PPU_50HZ (1<<4) +.EQU BANK_SRAM $70 +.EQU PPU_50HZ (1<<4) -.EQU INT_VBLENABLE (1<<7) -.EQU INT_JOYPAD_ENABLE (1) +.EQU INT_VBLENABLE (1<<7) +.EQU INT_JOYPAD_ENABLE (1) -.DEFINE TXT_VRAMADR $0800 -.DEFINE TXT_VRAMBGADR $0800 -.DEFINE TXT_VRAMOFFSET $0000 +.DEFINE TXT_VRAMADR $3000 +.DEFINE TXT_VRAMBGADR $6800 +.DEFINE TXT_VRAMOFFSET $0100 .BASE $00 .RAMSECTION ".reg_cons7e" BANK $7E SLOT RAMSLOT_0 @@ -378,26 +378,6 @@ consoleVblank: pha plb - ; Refresh pad values - lda snes_mplay5 - beq + - jsl scanMPlay5 - bra cvbloam -+ lda snes_mouse - beq + - jsl mouseRead - lda mouseConnect - and mouseConnect + 1 ; If both ports have a mouse plugged, it will skip pad controller reading - bne cvbloam -+ jsl scanPads - lda snes_sscope - beq cvbloam - jsl scanScope - -cvbloam: - ; Put oam to screen if needed - jsl oamUpdate - ; if buffer need to be update, do it ! lda scr_txt_dirty beq + diff --git a/pvsneslib/source/crt0_snes.asm b/pvsneslib/source/crt0_snes.asm index 2ee40991..ac1efcb0 100644 --- a/pvsneslib/source/crt0_snes.asm +++ b/pvsneslib/source/crt0_snes.asm @@ -221,6 +221,10 @@ tcc__snesinit: ; Needed to satisfy interrupt definition in "Header.inc". .SECTION ".vblank" SEMIFREE ORG ORG_0 +.accu 16 +.index 16 +.16bit + VBlank: .ifdef FASTROM jml FVBlank @@ -237,6 +241,45 @@ FVBlank: pea $7e7e plb plb + + ; Refresh pad values + sep #$20 + lda snes_mplay5 + beq + + jsl scanMPlay5 + bra cvbloam ++ + lda snes_mouse + beq + + jsl mouseRead + lda mouseConnect + and mouseConnect + 1 ; If both ports have a mouse plugged, it will skip pad controller reading + bne cvbloam ++ + jsl scanPads + lda snes_sscope + beq cvbloam + jsl scanScope + +cvbloam: + ; Put oam to screen if needed + rep #$20 ; A 16 bits + lda.w #$0000 + sta.l $2102 ; OAM address + lda.w #$0400 + sta.l $4370 ; DMA type CPU -> PPU, auto inc, $2104 (OAM write) + lda.w #$0220 + sta.l $4375 ; DMA size (220 = 128*4+32 + + lda #oamMemory.w + sta.l $4372 ; DMA address = oam memory + sep #$20 + lda #:oamMemory + sta.l $4374 ; DMA address bank = oam memory + + lda.b #$80 ; DMA channel 7 1xxx xxxx + sta.l $420b + rep #$20 ; Count frame number @@ -343,6 +386,8 @@ fast_start: stz.w snes_frame_count stz.w snes_frame_count_svg + jsl consoleInit + jsr.l main ; write exit code to $fffd From 0b6ce0d0d22a9640ed7a399e271642a5826b6714 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Thu, 9 May 2024 16:10:28 +0200 Subject: [PATCH 060/106] fix(*): fix oamFixN functions, they were not working --- pvsneslib/source/sprites.asm | 108 +++++++++++++++++------------------ 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/pvsneslib/source/sprites.asm b/pvsneslib/source/sprites.asm index bf2b39a1..319e0ddd 100644 --- a/pvsneslib/source/sprites.asm +++ b/pvsneslib/source/sprites.asm @@ -1464,13 +1464,12 @@ oamFix32Draw: lda 10,s ; get id asl a ; to be on correct index (16 bytes per oam) asl a + tax + phx ; save oam sprite entry id*4 asl a asl a tay - ldx oamnumberperframe ; get current sprite number (x4 entry) - - phx lda oambuffer.1.oamframeid,y ; get graphics offset of 32x32 sprites asl a tax @@ -1512,18 +1511,14 @@ _o32FRep3: sta oamMemory+3,x ; store attributes in oam memory rep #$20 ; oamSetEx(nb_sprites_oam, OBJ_SMALL, OBJ_SHOW); - lda oamnumberperframe ; always small for 8px sprites - lsr a - lsr a + lda 10,s ; always large for 32px sprites lsr a lsr a clc adc.w #512 ; id>>4 + 512 tay ; oam pointer is now on oam table #2 - lda oamnumberperframe ; id - lsr a - lsr a + lda 10,s ; id and.w #$0003 ; id >> 2 & 3 tax @@ -1536,12 +1531,15 @@ _o32FRep3: ora oamMemory,y sta oamMemory,y ; store new value in oam table #2 - rep #$20 - lda oamnumberperframe ; go to next sprite entry (x4 multiplier) - clc - adc #$0004 + rep #$20 ; change number of sprite to transfert if ID is more than max + lda 10,s + asl a + asl a + cmp oamnumberperframe + bcs _o32FRep2p0 ; no, leave the function sta.w oamnumberperframe - + +_o32FRep2p0: ply plx @@ -1640,7 +1638,7 @@ _o16DRep1: lda.l lkup16idT0,x _o16DRep1p: plx - sta.w oamMemory+2,x ; store in oam memory + sta.w oamMemory+2,x ; store in oam memory lda oambuffer.1.oamx,y ; get x coordinate xba ; save it @@ -1747,29 +1745,31 @@ oamFix16Draw: lda 10,s ; get id asl a ; to be on correct index (16 bytes per oam) asl a + tax + phx ; save oam sprite entry id*4 asl a asl a tay - ldx oambuffer.1.oamframeid,y ; get current sprite number (x4 entry) + lda oambuffer.1.oamframeid,y ; get current graphic number - phx + pha lda spr16addrgfx ; if large sprite, adjust address cmp spr0addrgfx beq + - lda oamnumberspr1 ; get address if not big sprite + pla ; get address if not big sprite asl a tax lda.l lkup16idT,x - brl _o16DRep1p + brl _o16FRep1p -+: lda oamnumberspr0 ; get address if big sprite ++: pla ; get address if big sprite asl a tax lda.l lkup16idT0,x -_o16DRep1p: +_o16FRep1p: plx - sta.w oamMemory+2,x ; store in oam memory + sta.w oamMemory+2,x ; store in oam memory id*4+2 TTTT TTTT - Low 8 bits of tile lda oambuffer.1.oamx,y ; get x coordinate xba ; save it @@ -1779,7 +1779,7 @@ _o16DRep1p: lda oambuffer.1.oamy,y ; get y coordinate xba rep #$20 ; A 16 bits - sta.w oamMemory,x ; store x & y in oam memory + sta.w oamMemory,x ; store x & y in oam memory id*4+0: XXXX XXXX - Low 8 bits of X position && id*4+1: YYYY YYYY - Y position lda.w #$0200 ; put $02 into MSB sep #$20 ; A 8 bits @@ -1787,14 +1787,14 @@ _o16DRep1p: phy tay - bcs _o16DRep3 ; if no x<255, no need to update + bcs _o16FRep3 ; if no x<255, no need to update lda.l oammask+1,x and.w oamMemory,y sta.w oamMemory,y ; store x in oam memory brl + -_o16DRep3: +_o16FRep3: lda.l oammask,x ora.w oamMemory,y sta.w oamMemory,y ; store x in oam memory @@ -1802,21 +1802,17 @@ _o16DRep3: +: ply lda oambuffer.1.oamattribute,y ; get attr - sta oamMemory+3,x ; store attributes in oam memory + sta oamMemory+3,x ; store attributes in oam memory id*4+3: VHPP CCCt rep #$20 ; oamSetEx(nb_sprites_oam, OBJ_SMALL, OBJ_SHOW); - lda oamnumberperframe - lsr a - lsr a + lda 10,s lsr a lsr a clc adc.w #512 ; id>>4 + 512 tay ; oam pointer is now on oam table #2 - lda oamnumberperframe ; id - lsr a - lsr a + lda 10,s ; id and.w #$0003 ; id >> 2 & 3 tax @@ -1828,19 +1824,22 @@ _o16DRep3: rep #$20 lda spr16addrgfx ; if large sprite, adjust it cmp spr1addrgfx - beq _o16DRep2p + beq _o16FRep2p sep #$20 lda.l oamSizeshift,x ; get shifted value of hide (<<1, <<3, <<5, <<7 ora oamMemory,y sta oamMemory,y ; store new value in oam table #2 - rep #$20 -_o16DRep2p: - lda oamnumberperframe ; go to next sprite entry (x4 multiplier) - clc - adc #$0004 +_o16FRep2p: + rep #$20 ; change number of sprite to transfert if ID is more than max + lda 10,s + asl a + asl a + cmp oamnumberperframe + bcs _o16FRep2p0 ; no, leave the function sta.w oamnumberperframe - + +_o16FRep2p0: ply plx @@ -2014,19 +2013,19 @@ oamFix8Draw: lda 10,s ; get id asl a ; to be on correct index (16 bytes per oam) asl a + tax + phx ; save oam sprite entry id*4 asl a asl a tay - ldx oamnumberperframe ; get current sprite number (x4 entry) + lda oambuffer.1.oamframeid,y ; get current graphic number - phx - lda oambuffer.1.oamframeid,y ; get graphics offset of 8x8 sprites asl a tax lda.l lkup8idT,x plx - sta.w oamMemory+2,x ; store in oam memory + sta.w oamMemory+2,x ; store in oam memory id*4+2 TTTT TTTT - Low 8 bits of tile lda oambuffer.1.oamx,y ; get x coordinate xba ; save it @@ -2036,7 +2035,7 @@ oamFix8Draw: lda oambuffer.1.oamy,y ; get y coordinate xba rep #$20 ; A 8 bits - sta.w oamMemory,x ; store x & y in oam memory + sta.w oamMemory,x ; store x & y in oam memory id*4+0: XXXX XXXX - Low 8 bits of X position && id*4+1: YYYY YYYY - Y position lda.w #$0200 ; put $02 into MSB sep #$20 ; A 8 bits @@ -2044,14 +2043,14 @@ oamFix8Draw: phy tay - bcs _o8DRep3 ; if no x<255, no need to update + bcs _o8FRep3 ; if no x<255, no need to update lda.l oammask+1,x and.w oamMemory,y sta.w oamMemory,y ; store x in oam memory brl + -_o8DRep3: +_o8FRep3: lda.l oammask,x ora.w oamMemory,y sta.w oamMemory,y ; store x in oam memory @@ -2062,18 +2061,14 @@ _o8DRep3: sta oamMemory+3,x ; store attributes in oam memory rep #$20 ; oamSetEx(nb_sprites_oam, OBJ_SMALL, OBJ_SHOW); - lda oamnumberperframe ; always small for 8px sprites - lsr a - lsr a + lda 10,s lsr a lsr a clc adc.w #512 ; id>>4 + 512 tay ; oam pointer is now on oam table #2 - lda oamnumberperframe ; id - lsr a - lsr a + lda 10,s ; id and.w #$0003 ; id >> 2 & 3 tax @@ -2083,11 +2078,14 @@ _o8DRep3: sta oamMemory,y ; store new value in oam table #2 rep #$20 - lda oamnumberperframe ; go to next sprite entry (x4 multiplier) - clc - adc #$0004 + lda 10,s + asl a + asl a + cmp oamnumberperframe + bcs _o8FRep2p0 ; no, leave the function sta.w oamnumberperframe +_o8FRep2p0: ply plx From 337533fa2ce7564061c3aff7e576ce998d7dbf71 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Mon, 13 May 2024 20:53:41 +1000 Subject: [PATCH 061/106] Read pads, mice and scope at the end of the VBlank ISR to free VBlank time that can be used by the `nmi_handler` --- pvsneslib/source/crt0_snes.asm | 43 +++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/pvsneslib/source/crt0_snes.asm b/pvsneslib/source/crt0_snes.asm index ac1efcb0..ceed8b4a 100644 --- a/pvsneslib/source/crt0_snes.asm +++ b/pvsneslib/source/crt0_snes.asm @@ -242,26 +242,7 @@ FVBlank: plb plb - ; Refresh pad values - sep #$20 - lda snes_mplay5 - beq + - jsl scanMPlay5 - bra cvbloam -+ - lda snes_mouse - beq + - jsl mouseRead - lda mouseConnect - and mouseConnect + 1 ; If both ports have a mouse plugged, it will skip pad controller reading - bne cvbloam -+ - jsl scanPads - lda snes_sscope - beq cvbloam - jsl scanScope -cvbloam: ; Put oam to screen if needed rep #$20 ; A 16 bits lda.w #$0000 @@ -292,6 +273,30 @@ cvbloam: lda.l nmi_handler + 2 sta.b tcc__r10h jsl tcc__jsl_r10 + + + ; Refresh pad values + sep #$20 + lda snes_mplay5 + beq + + jsl scanMPlay5 + bra @EndScanPads ++ + lda snes_mouse + beq + + jsl mouseRead + lda mouseConnect + and mouseConnect + 1 ; If both ports have a mouse plugged, it will skip pad controller reading + bne @EndScanPads ++ + jsl scanPads + lda snes_sscope + beq @EndScanPads + jsl scanScope +@EndScanPads: + + rep #$30 + pla ply plx From 1a26e620b960c0faab107b7921ebcc8b69550e01 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Mon, 13 May 2024 21:08:57 +1000 Subject: [PATCH 062/106] Fix high byte of pad_keys not cleared if joypad is not a standard controller --- pvsneslib/source/input.asm | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pvsneslib/source/input.asm b/pvsneslib/source/input.asm index bc3aea79..38087a4c 100644 --- a/pvsneslib/source/input.asm +++ b/pvsneslib/source/input.asm @@ -147,10 +147,9 @@ scanPads: lda REG_JOY1L ; read joypad register #1 bit #$0F ; catch non-joypad input beq + ; (bits 0-3 should be zero) - sep #$20 - lda #$0 - rep #$20 -+: sta pad_keys ; store 'current' state + lda.w #$0 ++ + sta pad_keys ; store 'current' state eor pad_keysold ; compute 'down' state from bits that and pad_keys ; have changed from 0 to 1 sta pad_keysrepeat ; @@ -158,10 +157,9 @@ scanPads: lda REG_JOY2L ; read joypad register #2 bit #$0F ; catch non-joypad input beq + ; (bits 0-3 should be zero) - sep #$20 - lda #$0 - rep #$20 -+: sta pad_keys+2 ; store 'current' state + lda.w #$0 ++ + sta pad_keys+2 ; store 'current' state eor pad_keysold+2 ; compute 'down' state from bits that and pad_keys+2 ; have changed from 0 to 1 sta pad_keysrepeat+2 ; From 95bc7ac6abfd16c202bac26ebfdccf54d71f155c Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Mon, 13 May 2024 21:22:14 +1000 Subject: [PATCH 063/106] Fix scanPads() and scanMPlay5() erroneously reading RDIO ($4213) --- pvsneslib/source/input.asm | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pvsneslib/source/input.asm b/pvsneslib/source/input.asm index 38087a4c..f806161a 100644 --- a/pvsneslib/source/input.asm +++ b/pvsneslib/source/input.asm @@ -134,15 +134,16 @@ scanPads: pha plb - rep #$20 ; copy joy states #1&2 - ldy pad_keys + ldy pad_keys ; copy joy states #1&2 sty pad_keysold ldy pad_keys+2 sty pad_keysold+2 --: lda REG_HVBJOY ; wait until joypads are ready - lsr - bcs - + lda #1 ; wait until joypads are ready +-: bit REG_HVBJOY + bne - + + rep #$20 lda REG_JOY1L ; read joypad register #1 bit #$0F ; catch non-joypad input @@ -370,11 +371,12 @@ scanMPlay5: ldy pad_keys+8 sty pad_keysold+8 --: lda REG_HVBJOY ; wait until joypads are ready - lsr - bcs - - sep #$20 + + lda #1 ; wait until joypads are ready +-: bit REG_HVBJOY + bne - + lda.b #$80 ; enable iobit to read data sta.w REG_WRIO From 499f3fd2856ae2b5da15eec4c722975d5d989749 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Tue, 14 May 2024 21:39:22 +1000 Subject: [PATCH 064/106] Split input.asm into input.asm and vblank.asm The functions that are called by the VBlank ISR have been moved to vblank.asm --- pvsneslib/source/input.asm | 445 +------------------------------- pvsneslib/source/libc.asm | 1 + pvsneslib/source/vblank.asm | 493 ++++++++++++++++++++++++++++++++++++ 3 files changed, 502 insertions(+), 437 deletions(-) create mode 100644 pvsneslib/source/vblank.asm diff --git a/pvsneslib/source/input.asm b/pvsneslib/source/input.asm index f806161a..b6bcdc4c 100644 --- a/pvsneslib/source/input.asm +++ b/pvsneslib/source/input.asm @@ -119,58 +119,6 @@ connect_st dsb 2 .ENDS -.BASE BASE_0 -.SECTION ".pads0_text" SUPERFREE - -;--------------------------------------------------------------------------------- -; void scanPads(void) -scanPads: - php - phb - phy - - sep #$20 ; change bank address to 0 - lda.b #$0 - pha - plb - - ldy pad_keys ; copy joy states #1&2 - sty pad_keysold - ldy pad_keys+2 - sty pad_keysold+2 - - lda #1 ; wait until joypads are ready --: bit REG_HVBJOY - bne - - - rep #$20 - - lda REG_JOY1L ; read joypad register #1 - bit #$0F ; catch non-joypad input - beq + ; (bits 0-3 should be zero) - lda.w #$0 -+ - sta pad_keys ; store 'current' state - eor pad_keysold ; compute 'down' state from bits that - and pad_keys ; have changed from 0 to 1 - sta pad_keysrepeat ; - - lda REG_JOY2L ; read joypad register #2 - bit #$0F ; catch non-joypad input - beq + ; (bits 0-3 should be zero) - lda.w #$0 -+ - sta pad_keys+2 ; store 'current' state - eor pad_keysold+2 ; compute 'down' state from bits that - and pad_keys+2 ; have changed from 0 to 1 - sta pad_keysrepeat+2 ; - - ply - plb - plp - rtl -.ENDS - .SECTION ".pads1_text" SUPERFREE @@ -345,276 +293,8 @@ nomplay5: .ENDS -.SECTION ".padsm51_text" SUPERFREE - -;--------------------------------------------------------------------------------- -; void scanMPlay5(void) -scanMPlay5: - php - phb - phy - - sep #$20 ; change bank address to 0 - lda.b #$0 - pha - plb - - rep #$20 ; copy joy states #1->5 - ldy pad_keys - sty pad_keysold - ldy pad_keys+2 - sty pad_keysold+2 - ldy pad_keys+4 - sty pad_keysold+4 - ldy pad_keys+6 - sty pad_keysold+6 - ldy pad_keys+8 - sty pad_keysold+8 - - sep #$20 - - lda #1 ; wait until joypads are ready --: bit REG_HVBJOY - bne - - - lda.b #$80 ; enable iobit to read data - sta.w REG_WRIO - - lda.b #$1 - sta.w REG_JOYA ; do stobe on/off - stz.w REG_JOYA - - rep #$20 - ldy #16 -getpad1data: ; get all 16 bits pad1 data serialy - lda.w REG_JOYA - lsr a ; put bit0 into carry - rol.w pad_keys ; pad 1 data - dey - bne getpad1data - - ldy #16 -getpad23data: ; get all 16 bits pad2&3 data serialy - lda.w REG_JOYB - lsr a ; put bit1 into carry - rol.w pad_keys+2 ; pad 2 data - lsr a ; put bit1 into carry - rol.w pad_keys+4 ; pad 3 data - dey - bne getpad23data - - sep #$20 - stz.w REG_WRIO ; to allow read for other pads - - rep #$20 - ldy #16 -getpad45data: ; get all 16 bits pad2&3 data serialy - lda.w REG_JOYB - lsr a ; put bit1 into carry - rol.w pad_keys+6 ; pad 4 data - lsr a ; put bit1 into carry - rol.w pad_keys+8 ; pad 5 data - dey - bne getpad45data - - lda pad_keys - eor pad_keysold ; compute 'down' state from bits that - and pad_keys ; have changed from 0 to 1 - sta pad_keysrepeat ; - lda pad_keys+2 - eor pad_keysold+2 - and pad_keys+2 - sta pad_keysrepeat+2 - lda pad_keys+4 - eor pad_keysold+4 - and pad_keys+4 - sta pad_keysrepeat+4 - lda pad_keys+6 - eor pad_keysold+6 - and pad_keys+6 - sta pad_keysrepeat+6 - lda pad_keys+8 - eor pad_keysold+8 - and pad_keys+8 - sta pad_keysrepeat+8 - - sep #$20 - lda.b #$80 ; enable iobit for next frame - sta.w REG_WRIO - ply - plb - plp - rtl - -.ENDS - -.SECTION ".padsscop_text" SUPERFREE - -;--------------------------------------------------------------------------------- -; Nintendo SHVC Scope BIOS version 1.00 -; Quickly disassembled and commented by Revenant on 31 Jan 2013 -; -; This assembly uses xkas v14 syntax. It probably also assembles with bass, if there's -; any such thing as good fortune in the universe. -; -; How to use the SHVC Super Scope BIOS: -; (all variables are two bytes) -; -; 1: Set "HoldDelay" and "RepDelay" for the button hold delay and repeat rate -; -; 2: "jsr GetScope" or "jsl GetScopeLong" once per frame -; -; 3: Read one of the following to get the scope input bits (see definitions below): -; - ScopeDown (for any flags that are currently true) -; - ScopeNow (for any flags that have become true this frame) -; - ScopeHeld (for any flags that have been true for a certain length of time) -; - ScopeLast (for any flags that were true on the previous frame) -; -; 3a: If the bits read from ScopeNow indicate a valid shot, or if the Cursor button -; is being pressed, then read "ShotH"/"ShotV" to adjust for aim, or read -; "ShotHRaw"/"ShotVRaw" for "pure" coordinates -; -; 3c: at some point, set "CenterH"/"CenterV" equal to "ShotHRaw"/"ShotVRaw" -; so that the aim-adjusted coordinates are "correct" -;--------------------------------------------------------------------------------- -; void scanScope(void) -scanScope: - phb - phk - plb - jsr GetScope - plb - rtl - -GetScope: - php - sep #$20 - - lda REG_STAT78 ; Has the PPU counter been latched? - and.b #$40 ; If not, don't get the scanline location - beq NoShot - - lda REG_OPHCT ; Get the horizontal scanline location (bits 0-7) - sta scope_shoth - sta scope_shothraw - - lda REG_OPHCT ; Get the horizontal scanline location (bit 8) - and.b #$01 - sta scope_shoth+1 - sta scope_shothraw+1 - - lda REG_OPVCT ; Get the vertical scanline location (bits 0-7) - sta scope_shotv - sta scope_shotvraw - - lda REG_OPVCT ; Get the vertical scanline location (bit 8) - and.b #$01 - sta scope_shotv+1 - sta scope_shotvraw+1 - - rep #$20 - lda scope_centerh ; Factor in the horizontal offset factor - clc - adc scope_shoth - sta scope_shoth - - lda scope_centerv ; Factor in the vertical offset factor - clc - adc scope_shotv - sta scope_shotv - - stz scope_sinceshot ; update number of frames since last shot - bra GetInput ; (what happens if 65536 frames elapse between shots?) - -NoShot: - inc scope_sinceshot - -; Wait for valid joypad input -GetInput: - sep #$20 - --: lda REG_HVBJOY - and.b #$01 - bne - - - rep #$20 - lda REG_JOY2L ; Get joypad 2 input - sta scope_port2down ; using a typical method to separate frame input / total input - eor scope_port2last - and scope_port2down - sta scope_port2now - lda scope_port2down - sta scope_port2last - - lda scope_port2down ; Check if the controller in port 2 is a Super Scope. - and.w #$0CFF ; For a 16-bit auto joypad read, bits 0-7 should be always 1 - cmp.w #$00FF ; and bits 10-11 should be always 0. - bne NoScope - - lda scope_sinceshot ; has a shot already happened this frame? - beq GetButtons ; If so, then only pay attention to the pause button bit - - lda scope_port2down ; Check which already-held buttons are still held - and scope_last - sta scope_last - - lda scope_port2down ; Check pause button held status - and.w #$1000 - sta scope_down - - lda scope_port2now ; Check pause button pressed status - and.w #$1000 - sta scope_now - - plp ; return from input check - rts - -GetButtons: - lda scope_port2down ; Get button status when NOT paused - sta scope_down - eor scope_last - and scope_port2down - sta scope_now - sta scope_held - - lda scope_port2down ; if no bits are set on port 2, don't check for "held buttons". - beq NotHolding - - cmp scope_last ; else if the bits aren't the same as last frame, don't check either. - bne NotHolding - - dec scope_tohold ; if a certain number of frames have elapsed with the same buttons - bne NotHeld ; held down, consider them "officially held". - - lda scope_port2down - sta scope_held - - lda scope_repdelay ; set the remaining delay to the repeat value - sta scope_tohold - bra NotHeld - -NotHolding: - lda scope_holddelay ; set the remaining delay to the normal value - sta scope_tohold - -NotHeld: - lda scope_port2down - sta scope_last - - plp ; return from input check - rts - -NoScope: - stz scope_port2down ; If no scope is connected, zero out all inputs - stz scope_port2now - stz scope_down - stz scope_now - stz scope_held - stz snes_sscope ; and lib flag - - plp ; return from input check - rts +.SECTION ".padsdetectsuperscope_text" SUPERFREE ;--------------------------------------------------------------------------------- ; detectSuperScope(void) @@ -673,121 +353,7 @@ detectSuperScope: ;--------------------------------------------------------------------------------- -.SECTION ".mouse_text" SUPERFREE - -;--------------------------------------------------------------------------------- -; void mouseRead(void) -mouseRead: - php - sep #$30 - phb - phx - phy - - lda #$00 ; Set Data Bank to 0 - pha - plb - -_10: - lda REG_HVBJOY - and #$01 - bne _10 ; Automatic read ok? - - ldx #$01 - lda REG_JOY2L ; Joy2 - jsr mouse_data - - lda connect_st+1 - beq _20 - - jsr speed_change - stz connect_st+1 - -_20: - dex - lda REG_JOY1L ; Joy1 - jsr mouse_data - - lda connect_st - beq _30 - - jsr speed_change - stz connect_st - -_30: - - lda mouseConnect - ora mouseConnect+1 - bne + - stz snes_mouse ; Disable mouse flag if no mouse connected - -+: - ply - plx - plb - plp - rtl - -mouse_data: - - sta tcc__r0 ; (421A / 4218 saved to reg0) - and.b #$0F - cmp.b #$01 ; Is the mouse connected? - beq _m10 - - stz mouseConnect,x ; No connection. - - stz mouseButton,x - stz mousePressed,x - stz mouse_x,x - stz mouse_y,x - - rts - -_m10: - lda mouseConnect,x ; When mouse is connected, speed will change. - bne _m20 ; Previous connection status - ; (mouse.com judged by lower 1 bit) - lda #$01 ; Connection check flag on - sta mouseConnect,x - sta connect_st,x - rts - -_m20: - rep #$10 - ldy #16 ; Read 16 bit data. - sep #$10 - -_m30: - lda REG_JOYA,x - - lsr a - rol mouse_x,x - rol mouse_y,x - dey - bne _m30 - - stz mousePressed,x - - rol tcc__r0 - rol mousePressed,x - rol tcc__r0 - rol mousePressed,x ; Switch turbo - - lda mousePressed,x - eor mouse_sb,x ; Get switch trigger - bne _m40 - - stz mouseButton,x - - rts - -_m40: - lda mousePressed,x - sta mouseButton,x - sta mouse_sb,x - - rts +.SECTION ".mousespeedchange_text" SUPERFREE ;--------------------------------------------------------------------------------- ; void mouseSpeedChange(u8 port) @@ -869,6 +435,11 @@ _s30: plp rts +.ENDS + + +.SECTION ".detectmouse_text" SUPERFREE + ;--------------------------------------------------------------------------------- ; detectMouse(void) detectMouse: @@ -900,4 +471,4 @@ detectMouse: plp rtl -.ENDS \ No newline at end of file +.ENDS diff --git a/pvsneslib/source/libc.asm b/pvsneslib/source/libc.asm index aa982d3e..3dca862c 100644 --- a/pvsneslib/source/libc.asm +++ b/pvsneslib/source/libc.asm @@ -483,3 +483,4 @@ exitl4: .include "sounds.asm" .include "sprites.asm" .include "videos.asm" +.include "vblank.asm" diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm new file mode 100644 index 00000000..5f1c0b9e --- /dev/null +++ b/pvsneslib/source/vblank.asm @@ -0,0 +1,493 @@ +;--------------------------------------------------------------------------------- +; +; Copyright (C) 2013-2024 +; Alekmaul - DigiDwrf +; +; This software is provided 'as-is', without any express or implied +; warranty. In no event will the authors be held liable for any +; damages arising from the use of this software. +; +; Permission is granted to anyone to use this software for any +; purpose, including commercial applications, and to alter it and +; redistribute it freely, subject to the following restrictions: +; +; 1. The origin of this software must not be misrepresented; you +; must not claim that you wrote the original software. If you use +; this software in a product, an acknowledgment in the product +; documentation would be appreciated but is not required. +; 2. Altered source versions must be plainly marked as such, and +; must not be misrepresented as being the original software. +; 3. This notice may not be removed or altered from any source +; distribution. +; +;--------------------------------------------------------------------------------- + + +.BASE BASE_0 +.SECTION ".pads0_text" SUPERFREE + +;--------------------------------------------------------------------------------- +; void scanPads(void) +scanPads: + php + phb + phy + + sep #$20 ; change bank address to 0 + lda.b #$0 + pha + plb + + ldy pad_keys ; copy joy states #1&2 + sty pad_keysold + ldy pad_keys+2 + sty pad_keysold+2 + + lda #1 ; wait until joypads are ready +-: bit REG_HVBJOY + bne - + + rep #$20 + + lda REG_JOY1L ; read joypad register #1 + bit #$0F ; catch non-joypad input + beq + ; (bits 0-3 should be zero) + lda.w #$0 ++ + sta pad_keys ; store 'current' state + eor pad_keysold ; compute 'down' state from bits that + and pad_keys ; have changed from 0 to 1 + sta pad_keysrepeat ; + + lda REG_JOY2L ; read joypad register #2 + bit #$0F ; catch non-joypad input + beq + ; (bits 0-3 should be zero) + lda.w #$0 ++ + sta pad_keys+2 ; store 'current' state + eor pad_keysold+2 ; compute 'down' state from bits that + and pad_keys+2 ; have changed from 0 to 1 + sta pad_keysrepeat+2 ; + + ply + plb + plp + rtl +.ENDS + + +.SECTION ".padsm51_text" SUPERFREE + +;--------------------------------------------------------------------------------- +; void scanMPlay5(void) +scanMPlay5: + php + phb + phy + + sep #$20 ; change bank address to 0 + lda.b #$0 + pha + plb + + rep #$20 ; copy joy states #1->5 + ldy pad_keys + sty pad_keysold + ldy pad_keys+2 + sty pad_keysold+2 + ldy pad_keys+4 + sty pad_keysold+4 + ldy pad_keys+6 + sty pad_keysold+6 + ldy pad_keys+8 + sty pad_keysold+8 + + sep #$20 + + lda #1 ; wait until joypads are ready +-: bit REG_HVBJOY + bne - + + lda.b #$80 ; enable iobit to read data + sta.w REG_WRIO + + lda.b #$1 + sta.w REG_JOYA ; do stobe on/off + stz.w REG_JOYA + + rep #$20 + ldy #16 +getpad1data: ; get all 16 bits pad1 data serialy + lda.w REG_JOYA + lsr a ; put bit0 into carry + rol.w pad_keys ; pad 1 data + dey + bne getpad1data + + ldy #16 +getpad23data: ; get all 16 bits pad2&3 data serialy + lda.w REG_JOYB + lsr a ; put bit1 into carry + rol.w pad_keys+2 ; pad 2 data + lsr a ; put bit1 into carry + rol.w pad_keys+4 ; pad 3 data + dey + bne getpad23data + + sep #$20 + stz.w REG_WRIO ; to allow read for other pads + + rep #$20 + ldy #16 +getpad45data: ; get all 16 bits pad2&3 data serialy + lda.w REG_JOYB + lsr a ; put bit1 into carry + rol.w pad_keys+6 ; pad 4 data + lsr a ; put bit1 into carry + rol.w pad_keys+8 ; pad 5 data + dey + bne getpad45data + + lda pad_keys + eor pad_keysold ; compute 'down' state from bits that + and pad_keys ; have changed from 0 to 1 + sta pad_keysrepeat ; + lda pad_keys+2 + eor pad_keysold+2 + and pad_keys+2 + sta pad_keysrepeat+2 + lda pad_keys+4 + eor pad_keysold+4 + and pad_keys+4 + sta pad_keysrepeat+4 + lda pad_keys+6 + eor pad_keysold+6 + and pad_keys+6 + sta pad_keysrepeat+6 + lda pad_keys+8 + eor pad_keysold+8 + and pad_keys+8 + sta pad_keysrepeat+8 + + sep #$20 + lda.b #$80 ; enable iobit for next frame + sta.w REG_WRIO + + ply + plb + plp + rtl + +.ENDS + +.SECTION ".padsscop_text" SUPERFREE + +;--------------------------------------------------------------------------------- +; Nintendo SHVC Scope BIOS version 1.00 +; Quickly disassembled and commented by Revenant on 31 Jan 2013 +; +; This assembly uses xkas v14 syntax. It probably also assembles with bass, if there's +; any such thing as good fortune in the universe. +; +; How to use the SHVC Super Scope BIOS: +; (all variables are two bytes) +; +; 1: Set "HoldDelay" and "RepDelay" for the button hold delay and repeat rate +; +; 2: "jsr GetScope" or "jsl GetScopeLong" once per frame +; +; 3: Read one of the following to get the scope input bits (see definitions below): +; - ScopeDown (for any flags that are currently true) +; - ScopeNow (for any flags that have become true this frame) +; - ScopeHeld (for any flags that have been true for a certain length of time) +; - ScopeLast (for any flags that were true on the previous frame) +; +; 3a: If the bits read from ScopeNow indicate a valid shot, or if the Cursor button +; is being pressed, then read "ShotH"/"ShotV" to adjust for aim, or read +; "ShotHRaw"/"ShotVRaw" for "pure" coordinates +; +; 3c: at some point, set "CenterH"/"CenterV" equal to "ShotHRaw"/"ShotVRaw" +; so that the aim-adjusted coordinates are "correct" +;--------------------------------------------------------------------------------- +; void scanScope(void) +scanScope: + phb + phk + plb + jsr GetScope + plb + rtl + +GetScope: + php + sep #$20 + + lda REG_STAT78 ; Has the PPU counter been latched? + and.b #$40 ; If not, don't get the scanline location + beq NoShot + + lda REG_OPHCT ; Get the horizontal scanline location (bits 0-7) + sta scope_shoth + sta scope_shothraw + + lda REG_OPHCT ; Get the horizontal scanline location (bit 8) + and.b #$01 + sta scope_shoth+1 + sta scope_shothraw+1 + + lda REG_OPVCT ; Get the vertical scanline location (bits 0-7) + sta scope_shotv + sta scope_shotvraw + + lda REG_OPVCT ; Get the vertical scanline location (bit 8) + and.b #$01 + sta scope_shotv+1 + sta scope_shotvraw+1 + + rep #$20 + lda scope_centerh ; Factor in the horizontal offset factor + clc + adc scope_shoth + sta scope_shoth + + lda scope_centerv ; Factor in the vertical offset factor + clc + adc scope_shotv + sta scope_shotv + + stz scope_sinceshot ; update number of frames since last shot + bra GetInput ; (what happens if 65536 frames elapse between shots?) + +NoShot: + inc scope_sinceshot + +; Wait for valid joypad input +GetInput: + sep #$20 + +-: lda REG_HVBJOY + and.b #$01 + bne - + + rep #$20 + lda REG_JOY2L ; Get joypad 2 input + sta scope_port2down ; using a typical method to separate frame input / total input + eor scope_port2last + and scope_port2down + sta scope_port2now + lda scope_port2down + sta scope_port2last + + lda scope_port2down ; Check if the controller in port 2 is a Super Scope. + and.w #$0CFF ; For a 16-bit auto joypad read, bits 0-7 should be always 1 + cmp.w #$00FF ; and bits 10-11 should be always 0. + bne NoScope + + lda scope_sinceshot ; has a shot already happened this frame? + beq GetButtons ; If so, then only pay attention to the pause button bit + + lda scope_port2down ; Check which already-held buttons are still held + and scope_last + sta scope_last + + lda scope_port2down ; Check pause button held status + and.w #$1000 + sta scope_down + + lda scope_port2now ; Check pause button pressed status + and.w #$1000 + sta scope_now + + plp ; return from input check + rts + +GetButtons: + lda scope_port2down ; Get button status when NOT paused + sta scope_down + eor scope_last + and scope_port2down + sta scope_now + sta scope_held + + lda scope_port2down ; if no bits are set on port 2, don't check for "held buttons". + beq NotHolding + + cmp scope_last ; else if the bits aren't the same as last frame, don't check either. + bne NotHolding + + dec scope_tohold ; if a certain number of frames have elapsed with the same buttons + bne NotHeld ; held down, consider them "officially held". + + lda scope_port2down + sta scope_held + + lda scope_repdelay ; set the remaining delay to the repeat value + sta scope_tohold + bra NotHeld + +NotHolding: + lda scope_holddelay ; set the remaining delay to the normal value + sta scope_tohold + +NotHeld: + lda scope_port2down + sta scope_last + + plp ; return from input check + rts + +NoScope: + stz scope_port2down ; If no scope is connected, zero out all inputs + stz scope_port2now + stz scope_down + stz scope_now + stz scope_held + stz snes_sscope ; and lib flag + + plp ; return from input check + rts + +.ENDS + +;--------------------------------------------------------------------------------- + +;* mouse_read + +;--------------------------------------------------------------------------------- + +;* If this routine is called every frame, then the mouse status will be set +;* to the appropriate registers. +;* INPUT +;* None (Mouse key read automatically) +;* OUTPUT +;* Connection status (mouse_con) D0=1 Mouse connected to Joyl +;* D1=1 Mouse connected to Joy2 +;* Switch (mousePressed,1) D0=left switch turbo +;* D1=right switch turbo +;* Switch (mouseButton,1) D0=left switch trigger +;* D1=right switch trigger +;* Mouse movement (ball) value +;* (mouse_x) D7=0 Positive turn, D7=1 Negative turn +;* D6-D0 X movement value +;* (mouse_y) D7=0 Positive turn, D7=1 Negative turn +;* D6-D0 X movement value + +;--------------------------------------------------------------------------------- + +.SECTION ".mouse_text" SUPERFREE + +;--------------------------------------------------------------------------------- +; void mouseRead(void) +mouseRead: + php + sep #$30 + phb + phx + phy + + lda #$00 ; Set Data Bank to 0 + pha + plb + +_10: + lda REG_HVBJOY + and #$01 + bne _10 ; Automatic read ok? + + ldx #$01 + lda REG_JOY2L ; Joy2 + jsr mouse_data + + lda connect_st+1 + beq _20 + + jsr speed_change + stz connect_st+1 + +_20: + dex + lda REG_JOY1L ; Joy1 + jsr mouse_data + + lda connect_st + beq _30 + + jsr speed_change + stz connect_st + +_30: + + lda mouseConnect + ora mouseConnect+1 + bne + + stz snes_mouse ; Disable mouse flag if no mouse connected + ++: + ply + plx + plb + plp + rtl + +mouse_data: + + sta tcc__r0 ; (421A / 4218 saved to reg0) + and.b #$0F + cmp.b #$01 ; Is the mouse connected? + beq _m10 + + stz mouseConnect,x ; No connection. + + stz mouseButton,x + stz mousePressed,x + stz mouse_x,x + stz mouse_y,x + + rts + +_m10: + lda mouseConnect,x ; When mouse is connected, speed will change. + bne _m20 ; Previous connection status + ; (mouse.com judged by lower 1 bit) + lda #$01 ; Connection check flag on + sta mouseConnect,x + sta connect_st,x + rts + +_m20: + rep #$10 + ldy #16 ; Read 16 bit data. + sep #$10 + +_m30: + lda REG_JOYA,x + + lsr a + rol mouse_x,x + rol mouse_y,x + dey + bne _m30 + + stz mousePressed,x + + rol tcc__r0 + rol mousePressed,x + rol tcc__r0 + rol mousePressed,x ; Switch turbo + + lda mousePressed,x + eor mouse_sb,x ; Get switch trigger + bne _m40 + + stz mouseButton,x + + rts + +_m40: + lda mousePressed,x + sta mouseButton,x + sta mouse_sb,x + + rts + +.ENDS From 574398204853ae402f232dd3e9126c5e39da82a9 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Tue, 14 May 2024 21:52:32 +1000 Subject: [PATCH 065/106] Move VBlank ISR/functions/variables to vblank.asm --- pvsneslib/source/crt0_snes.asm | 93 -------------------- pvsneslib/source/interrupts.asm | 66 --------------- pvsneslib/source/libc.asm | 1 - pvsneslib/source/vblank.asm | 146 ++++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+), 160 deletions(-) delete mode 100644 pvsneslib/source/interrupts.asm diff --git a/pvsneslib/source/crt0_snes.asm b/pvsneslib/source/crt0_snes.asm index ceed8b4a..ead5ea3c 100644 --- a/pvsneslib/source/crt0_snes.asm +++ b/pvsneslib/source/crt0_snes.asm @@ -25,16 +25,10 @@ tcc__f3 dsb 2 tcc__f3h dsb 2 move_insn dsb 4 ; 3 bytes mvn + 1 byte rts move_backwards_insn dsb 4 ; 3 bytes mvp + 1 byte rts -nmi_handler dsb 4 tcc__registers_irq dsb 0 tcc__regs_irq dsb 48 -snes_vblank_count dsb 2 ; 2 bytes to count number of vblank -snes_vblank_count_svg dsb 2 ; same thing for saving purpose -snes_frame_count dsb 2 ; 2 bytes for frame counter inside loop -snes_frame_count_svg dsb 2 ; same thing for saving purpose - .ENDS ; sections "globram.data" and "glob.data" can stay here in the file @@ -218,93 +212,6 @@ tcc__snesinit: .ENDS -; Needed to satisfy interrupt definition in "Header.inc". -.SECTION ".vblank" SEMIFREE ORG ORG_0 - -.accu 16 -.index 16 -.16bit - -VBlank: -.ifdef FASTROM - jml FVBlank - -FVBlank: -.endif - rep #$30 - phb - phd - phx - phy - pha - ; set data bank register to bss section - pea $7e7e - plb - plb - - - ; Put oam to screen if needed - rep #$20 ; A 16 bits - lda.w #$0000 - sta.l $2102 ; OAM address - lda.w #$0400 - sta.l $4370 ; DMA type CPU -> PPU, auto inc, $2104 (OAM write) - lda.w #$0220 - sta.l $4375 ; DMA size (220 = 128*4+32 - - lda #oamMemory.w - sta.l $4372 ; DMA address = oam memory - sep #$20 - lda #:oamMemory - sta.l $4374 ; DMA address bank = oam memory - - lda.b #$80 ; DMA channel 7 1xxx xxxx - sta.l $420b - - rep #$20 - - ; Count frame number - inc.w snes_vblank_count - - lda.w #tcc__registers_irq - tad - lda.l nmi_handler - sta.b tcc__r10 - lda.l nmi_handler + 2 - sta.b tcc__r10h - jsl tcc__jsl_r10 - - - ; Refresh pad values - sep #$20 - lda snes_mplay5 - beq + - jsl scanMPlay5 - bra @EndScanPads -+ - lda snes_mouse - beq + - jsl mouseRead - lda mouseConnect - and mouseConnect + 1 ; If both ports have a mouse plugged, it will skip pad controller reading - bne @EndScanPads -+ - jsl scanPads - lda snes_sscope - beq @EndScanPads - jsl scanScope -@EndScanPads: - - rep #$30 - - pla - ply - plx - pld - plb - RTI - -.ENDS .SECTION ".start" SEMIFREE ORG ORG_0 diff --git a/pvsneslib/source/interrupts.asm b/pvsneslib/source/interrupts.asm deleted file mode 100644 index da78dd83..00000000 --- a/pvsneslib/source/interrupts.asm +++ /dev/null @@ -1,66 +0,0 @@ -;--------------------------------------------------------------------------------- -; -; Copyright (C) 2013-2020 -; Alekmaul -; -; This software is provided 'as-is', without any express or implied -; warranty. In no event will the authors be held liable for any -; damages arising from the use of this software. -; -; Permission is granted to anyone to use this software for any -; purpose, including commercial applications, and to alter it and -; redistribute it freely, subject to the following restrictions: -; -; 1. The origin of this software must not be misrepresented; you -; must not claim that you wrote the original software. If you use -; this software in a product, an acknowledgment in the product -; documentation would be appreciated but is not required. -; 2. Altered source versions must be plainly marked as such, and -; must not be misrepresented as being the original software. -; 3. This notice may not be removed or altered from any source -; distribution. -; -;--------------------------------------------------------------------------------- - -.BASE BASE_0 -.SECTION ".interrupts0_text" SUPERFREE - -;--------------------------------------------------------------------------- -WaitForVBlank: - wai - rtl - -; old version still here for memory purpose -; pha -; php -; sep #$20 -;-: -; lda.l REG_RDNMI -; bmi - -;-: -; lda.l REG_RDNMI -; bpl - -; plp -; pla -; rtl - - -.ENDS - -.SECTION ".interrupts1_text" SUPERFREE - -;--------------------------------------------------------------------------- -; void WaitNVBlank(u16 ntime) -WaitNVBlank: - php - - sep #$20 - lda 5,s -- wai - dea - bne - - - plp - rtl - -.ENDS diff --git a/pvsneslib/source/libc.asm b/pvsneslib/source/libc.asm index 3dca862c..a54a917c 100644 --- a/pvsneslib/source/libc.asm +++ b/pvsneslib/source/libc.asm @@ -474,7 +474,6 @@ exitl4: .include "consoles.asm" .include "dmas.asm" .include "input.asm" -.include "interrupts.asm" .include "lzsss.asm" .include "maps.asm" .include "objects.asm" diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 5f1c0b9e..bf593bcf 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -23,6 +23,18 @@ ;--------------------------------------------------------------------------------- +.RAMSECTION ".registers" BANK 0 SLOT 1 PRIORITY 1 + +nmi_handler dsb 4 + +snes_vblank_count dsb 2 ; 2 bytes to count number of vblank +snes_vblank_count_svg dsb 2 ; same thing for saving purpose +snes_frame_count dsb 2 ; 2 bytes for frame counter inside loop +snes_frame_count_svg dsb 2 ; same thing for saving purpose + +.ENDS + + .BASE BASE_0 .SECTION ".pads0_text" SUPERFREE @@ -491,3 +503,137 @@ _m40: rts .ENDS + + +; Needed to satisfy interrupt definition in "Header.inc". +.SECTION ".vblank" SEMIFREE ORG ORG_0 + +.accu 16 +.index 16 +.16bit + +VBlank: +.ifdef FASTROM + jml FVBlank + +FVBlank: +.endif + rep #$30 + phb + phd + phx + phy + pha + ; set data bank register to bss section + pea $7e7e + plb + plb + + + ; Put oam to screen if needed + rep #$20 ; A 16 bits + lda.w #$0000 + sta.l $2102 ; OAM address + lda.w #$0400 + sta.l $4370 ; DMA type CPU -> PPU, auto inc, $2104 (OAM write) + lda.w #$0220 + sta.l $4375 ; DMA size (220 = 128*4+32 + + lda #oamMemory.w + sta.l $4372 ; DMA address = oam memory + sep #$20 + lda #:oamMemory + sta.l $4374 ; DMA address bank = oam memory + + lda.b #$80 ; DMA channel 7 1xxx xxxx + sta.l $420b + + rep #$20 + + ; Count frame number + inc.w snes_vblank_count + + lda.w #tcc__registers_irq + tad + lda.l nmi_handler + sta.b tcc__r10 + lda.l nmi_handler + 2 + sta.b tcc__r10h + jsl tcc__jsl_r10 + + + ; Refresh pad values + sep #$20 + lda snes_mplay5 + beq + + jsl scanMPlay5 + bra @EndScanPads ++ + lda snes_mouse + beq + + jsl mouseRead + lda mouseConnect + and mouseConnect + 1 ; If both ports have a mouse plugged, it will skip pad controller reading + bne @EndScanPads ++ + jsl scanPads + lda snes_sscope + beq @EndScanPads + jsl scanScope +@EndScanPads: + + rep #$30 + + pla + ply + plx + pld + plb + RTI + +.ENDS + + +.BASE BASE_0 +.SECTION ".waitforvblank_text" SUPERFREE + +;--------------------------------------------------------------------------- +WaitForVBlank: + wai + rtl + +; old version still here for memory purpose +; pha +; php +; sep #$20 +;-: +; lda.l REG_RDNMI +; bmi - +;-: +; lda.l REG_RDNMI +; bpl - +; plp +; pla +; rtl + +.ENDS + + +.SECTION ".waitnvblank_text" SUPERFREE + +;--------------------------------------------------------------------------- +; void WaitNVBlank(u16 ntime) +WaitNVBlank: + php + + sep #$20 + lda 5,s +- wai + dea + bne - + + plp + rtl + +.ENDS + From 0147d3f21d156560334f0722fdbbc12aa1eaf249 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Tue, 14 May 2024 22:16:02 +1000 Subject: [PATCH 066/106] Fix VBlank ISR not in bank 0 --- pvsneslib/source/vblank.asm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index bf593bcf..397af55e 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -506,7 +506,7 @@ _m40: ; Needed to satisfy interrupt definition in "Header.inc". -.SECTION ".vblank" SEMIFREE ORG ORG_0 +.SECTION ".vblank" SEMIFREE ORG ORG_0 BANK 0 .accu 16 .index 16 From c43a70e7352f520044b70702f908b909e63768c4 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Tue, 14 May 2024 22:20:03 +1000 Subject: [PATCH 067/106] Remove scan pads/MPlay5/Scope/Mouse from input.h These functions are called by the VBlank ISR and should not be called multiple times per frame. --- pvsneslib/include/snes/input.h | 46 +------------------ pvsneslib/source/vblank.asm | 16 +++---- snes-examples/audio/effects/effects.c | 3 +- .../audio/effectsandmusic/effectsandmusic.c | 3 +- snes-examples/audio/tada/tada.c | 3 +- .../Mode1ContinuosScroll.c | 1 - .../Backgrounds/Mode1Scroll/Mode1Scroll.c | 3 -- .../Sprites/DynamicSprite/DynamicSprite.c | 3 -- 8 files changed, 12 insertions(+), 66 deletions(-) diff --git a/pvsneslib/include/snes/input.h b/pvsneslib/include/snes/input.h index d4c8278f..14e3d468 100644 --- a/pvsneslib/include/snes/input.h +++ b/pvsneslib/include/snes/input.h @@ -147,11 +147,6 @@ extern u16 scope_sinceshot; /*! \brief Number of frames elapsed since last shot */ #define REG_JOYxLH(a) (((vuint16 *)0x4218)[(a)]) -/*! \fn scanPads() - \brief Wait for pad ready and read pad values in. -*/ -void scanPads(void); - /*! \fn padsCurrent(value) \brief Return current value of selected pad \param value Address of the pad to use (0 or 1 to 4 if multiplayer 5 connected) @@ -185,21 +180,11 @@ void padsClear(u16 value); */ void detectMPlay5(void); -/*! \fn scanMPlay5() - \brief Wait for multiplayer5 pads ready and read pad values in. -*/ -void scanMPlay5(void); - /*! \fn detectMouse(void) \brief Check if Mouse is connected and populate snes_mouse (0 or 1 for connected) */ void detectMouse(void); -/*! \fn mouseRead(void) - \brief Wait for mouse ready and read mouse values in. -*/ -void mouseRead(void); - /*! \fn mouseSpeedChange(u8 port) \brief Set mouse hardware speed (populate mouseSpeed[] first). \param port Specify wich port to use (0-1) @@ -211,33 +196,4 @@ void mouseSpeedChange(u8 port); */ void detectSuperScope(void); -/*! \fn scanScope(void) - \brief Nintendo SHVC Scope BIOS version 1.00 - Quickly disassembled and commented by Revenant on 31 Jan 2013 - - This assembly uses xkas v14 syntax. It probably also assembles with bass, if there's - any such thing as good fortune in the universe. - - How to use the SHVC Super Scope BIOS: - (all variables are two bytes) - - 1: Set "HoldDelay" and "RepDelay" for the button hold delay and repeat rate - - 2: "jsr GetScope" or "jsl GetScopeLong" once per frame - - 3: Read one of the following to get the scope input bits (see definitions below): - - ScopeDown (for any flags that are currently true) - - ScopeNow (for any flags that have become true this frame) - - ScopeHeld (for any flags that have been true for a certain length of time) - - ScopeLast (for any flags that were true on the previous frame) - - 3a: If the bits read from ScopeNow indicate a valid shot, or if the Cursor button - is being pressed, then read "ShotH"/"ShotV" to adjust for aim, or read - "ShotHRaw"/"ShotVRaw" for "pure" coordinates - - 3c: at some point, set "CenterH"/"CenterV" equal to "ShotHRaw"/"ShotVRaw" - so that the aim-adjusted coordinates are "correct" -*/ -void scanScope(void); - -#endif // SNES_PADS_INCLUDE \ No newline at end of file +#endif // SNES_PADS_INCLUDE diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 397af55e..14bd2210 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -40,7 +40,7 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose ;--------------------------------------------------------------------------------- ; void scanPads(void) -scanPads: +scanPads_: php phb phy @@ -92,7 +92,7 @@ scanPads: ;--------------------------------------------------------------------------------- ; void scanMPlay5(void) -scanMPlay5: +scanMPlay5_: php phb phy @@ -222,7 +222,7 @@ getpad45data: ; get all 16 bits pad2&3 data serialy ; so that the aim-adjusted coordinates are "correct" ;--------------------------------------------------------------------------------- ; void scanScope(void) -scanScope: +scanScope_: phb phk plb @@ -390,7 +390,7 @@ NoScope: ;--------------------------------------------------------------------------------- ; void mouseRead(void) -mouseRead: +mouseRead_: php sep #$30 phb @@ -566,20 +566,20 @@ FVBlank: sep #$20 lda snes_mplay5 beq + - jsl scanMPlay5 + jsl scanMPlay5_ bra @EndScanPads + lda snes_mouse beq + - jsl mouseRead + jsl mouseRead_ lda mouseConnect and mouseConnect + 1 ; If both ports have a mouse plugged, it will skip pad controller reading bne @EndScanPads + - jsl scanPads + jsl scanPads_ lda snes_sscope beq @EndScanPads - jsl scanScope + jsl scanScope_ @EndScanPads: rep #$30 diff --git a/snes-examples/audio/effects/effects.c b/snes-examples/audio/effects/effects.c index 2404b61b..e0a2da47 100644 --- a/snes-examples/audio/effects/effects.c +++ b/snes-examples/audio/effects/effects.c @@ -62,8 +62,7 @@ int main(void) consoleDrawText(7, 14, "Effect: tada"); while (1) { - // Refresh pad values and test key a (without repeating sound if still pressed) - scanPads(); + // Test key a (without repeating sound if still pressed) if (padsCurrent(0) & KEY_A) { if (keyapressed == 0) diff --git a/snes-examples/audio/effectsandmusic/effectsandmusic.c b/snes-examples/audio/effectsandmusic/effectsandmusic.c index 4e9f5b98..01a0d52c 100644 --- a/snes-examples/audio/effectsandmusic/effectsandmusic.c +++ b/snes-examples/audio/effectsandmusic/effectsandmusic.c @@ -68,8 +68,7 @@ int main(void) consoleDrawText(5, 17, "Effect: tada"); while (1) { - // Refresh pad values and test key a (without repeating sound if still pressed) - scanPads(); + // Test key a (without repeating sound if still pressed) if (padsCurrent(0) & KEY_A) { if (keyapressed == 0) diff --git a/snes-examples/audio/tada/tada.c b/snes-examples/audio/tada/tada.c index 23b750e0..42af406e 100644 --- a/snes-examples/audio/tada/tada.c +++ b/snes-examples/audio/tada/tada.c @@ -53,8 +53,7 @@ int main(void) // Wait for nothing :D ! while (1) { - // Refresh pad values and test key a (without repeating sound if still pressed) - scanPads(); + // Test key a (without repeating sound if still pressed) if (padsCurrent(0) & KEY_A) { if (keyapressed == 0) diff --git a/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c b/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c index 2a85d2a9..a8d7ad54 100644 --- a/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c +++ b/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c @@ -277,7 +277,6 @@ int main(void) // Wait for nothing :P while (1) { - scanPads(); pad0 = padsCurrent(0); // update character pos updatePos(&player1, pad0); diff --git a/snes-examples/graphics/Backgrounds/Mode1Scroll/Mode1Scroll.c b/snes-examples/graphics/Backgrounds/Mode1Scroll/Mode1Scroll.c index 7be49a56..0719b373 100644 --- a/snes-examples/graphics/Backgrounds/Mode1Scroll/Mode1Scroll.c +++ b/snes-examples/graphics/Backgrounds/Mode1Scroll/Mode1Scroll.c @@ -47,9 +47,6 @@ int main(void) // no move currently move = 0; - // Refresh pad values - scanPads(); - // Get current #0 pad pad0 = padsCurrent(0); diff --git a/snes-examples/graphics/Sprites/DynamicSprite/DynamicSprite.c b/snes-examples/graphics/Sprites/DynamicSprite/DynamicSprite.c index 2f3c29c1..e053e685 100644 --- a/snes-examples/graphics/Sprites/DynamicSprite/DynamicSprite.c +++ b/snes-examples/graphics/Sprites/DynamicSprite/DynamicSprite.c @@ -30,9 +30,6 @@ void myconsoleVblank(void) u8 *pgfx; u16 padrgfx; - // Refresh pad values - scanPads(); - // if tile sprite queued if (spr_queue != 0xff) { From 1084656801452be996c9e4722f6c56aa4e1bce72 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Tue, 14 May 2024 23:16:29 +1000 Subject: [PATCH 068/106] Optimise VBlank input by not changing DB or pushing/popping registers --- pvsneslib/source/vblank.asm | 97 ++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 55 deletions(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 14bd2210..4f7f14fb 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -38,18 +38,15 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose .BASE BASE_0 .SECTION ".pads0_text" SUPERFREE -;--------------------------------------------------------------------------------- -; void scanPads(void) +;; Scan and read the joypads +;; +;; REQUIRES: Auto-Joypad enabled. +;; +;; ACCU 8 +;; INDEX 16 +;; DB = 0 +;; D = tcc__registers_irq (NOT ZERO) scanPads_: - php - phb - phy - - sep #$20 ; change bank address to 0 - lda.b #$0 - pha - plb - ldy pad_keys ; copy joy states #1&2 sty pad_keysold ldy pad_keys+2 @@ -81,27 +78,23 @@ scanPads_: and pad_keys+2 ; have changed from 0 to 1 sta pad_keysrepeat+2 ; - ply - plb - plp + sep #$20 rtl .ENDS .SECTION ".padsm51_text" SUPERFREE -;--------------------------------------------------------------------------------- -; void scanMPlay5(void) -scanMPlay5_: - php - phb - phy - - sep #$20 ; change bank address to 0 - lda.b #$0 - pha - plb +;; Scan and read the multiplayer5 pads +;; +;; REQUIRES: Auto-Joypad enabled. +;; +;; ACCU 8 +;; INDEX 16 +;; DB = 0 +;; D = tcc__registers_irq (NOT ZERO) +scanMPlay5_: rep #$20 ; copy joy states #1->5 ldy pad_keys sty pad_keysold @@ -185,14 +178,11 @@ getpad45data: ; get all 16 bits pad2&3 data serialy lda.b #$80 ; enable iobit for next frame sta.w REG_WRIO - ply - plb - plp rtl - .ENDS -.SECTION ".padsscop_text" SUPERFREE + +.SECTION ".padsscop_text" SEMIFREE ORG ORG_0 BANK 0 ;--------------------------------------------------------------------------------- ; Nintendo SHVC Scope BIOS version 1.00 @@ -221,16 +211,7 @@ getpad45data: ; get all 16 bits pad2&3 data serialy ; 3c: at some point, set "CenterH"/"CenterV" equal to "ShotHRaw"/"ShotVRaw" ; so that the aim-adjusted coordinates are "correct" ;--------------------------------------------------------------------------------- -; void scanScope(void) -scanScope_: - phb - phk - plb - jsr GetScope - plb - rtl - -GetScope: +GetScope_: php sep #$20 @@ -386,20 +367,19 @@ NoScope: ;--------------------------------------------------------------------------------- + .SECTION ".mouse_text" SUPERFREE -;--------------------------------------------------------------------------------- -; void mouseRead(void) +;; Read the mouse values +;; +;; REQUIRES: Auto-Joypad enabled. +;; +;; ACCU 8 +;; INDEX 16 +;; DB = 0 +;; D = tcc__registers_irq (NOT ZERO) mouseRead_: - php sep #$30 - phb - phx - phy - - lda #$00 ; Set Data Bank to 0 - pha - plb _10: lda REG_HVBJOY @@ -435,12 +415,12 @@ _30: stz snes_mouse ; Disable mouse flag if no mouse connected +: - ply - plx - plb - plp + rep #$10 rtl + +.accu 8 +.index 8 mouse_data: sta tcc__r0 ; (421A / 4218 saved to reg0) @@ -564,6 +544,13 @@ FVBlank: ; Refresh pad values sep #$20 + + ; Set Data Bank to access joypad registers and lowram + lda.b #0 + pha + plb +; DB = 0 + lda snes_mplay5 beq + jsl scanMPlay5_ @@ -579,7 +566,7 @@ FVBlank: jsl scanPads_ lda snes_sscope beq @EndScanPads - jsl scanScope_ + jsr GetScope_ @EndScanPads: rep #$30 From 3a3db7098c132a28e88e8350151fb94abbf4b1b7 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Thu, 16 May 2024 19:34:31 +1000 Subject: [PATCH 069/106] Fix scanMplay5 The getpad1data loop did a 16-bit read of REG_JOYA, which also reads from REG_JOYB, preventing the getpad23data loop from reading joypad values. scanMplay5 now tests if a standard controller is connected to the multitap. This commit also optimises the multitap reading code by: * Using Joypad Auto-Read to read the first three controllers. * Using ScanPads to read the first two controllers. * Using an 8 bit A when reading the last two controllers. * Using the carry output of `rol` to determine when all of the controller bits have been read. As a consequence of the Joypad Auto-Read optimisation, the WRIO register must not be written to when `snes_mplay5` is set. I do not think this will be an issue, since `REG_WRIO` (nor 4201) is not defined in the header files nor the snes-examples directory. Tested by running the multiplay5 example in Mesen --- pvsneslib/include/snes/input.h | 3 + pvsneslib/source/input.asm | 1 + pvsneslib/source/vblank.asm | 177 +++++++++++++++++++-------------- 3 files changed, 104 insertions(+), 77 deletions(-) diff --git a/pvsneslib/include/snes/input.h b/pvsneslib/include/snes/input.h index 14e3d468..25a2e7f3 100644 --- a/pvsneslib/include/snes/input.h +++ b/pvsneslib/include/snes/input.h @@ -177,6 +177,9 @@ void padsClear(u16 value); /*! \fn detectMPlay5(void) \brief Check if MultiPlayer5 is connected and populate snes_mplay5 (0 or 1 for connected) + + \b CAUTION: REG_WRIO ($4201) must not be written to while MultiPlayer5 is active. + (Bit 7 of REG_WRIO must be set when Auto Joy reads the controllers, shortly after the VBlank Period starts.) */ void detectMPlay5(void); diff --git a/pvsneslib/source/input.asm b/pvsneslib/source/input.asm index b6bcdc4c..ca71356d 100644 --- a/pvsneslib/source/input.asm +++ b/pvsneslib/source/input.asm @@ -30,6 +30,7 @@ .equ REG_JOY1L $4218 .equ REG_JOY2L $421A +.equ REG_JOY4L $421E .equ REG_OPHCT $213C ; Horizontal scanline location diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 4f7f14fb..f03f1bc5 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -41,6 +41,7 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose ;; Scan and read the joypads ;; ;; REQUIRES: Auto-Joypad enabled. +;; REQUIRES: REG_WRIO bit 7 set BEFORE Vblank starts if a Multitap/Mp5 is connected. ;; ;; ACCU 8 ;; INDEX 16 @@ -86,97 +87,118 @@ scanPads_: .SECTION ".padsm51_text" SUPERFREE -;; Scan and read the multiplayer5 pads +;; Scan and read the last 3 controllers on a Multitap or MP5 connected to Port 2. ;; -;; REQUIRES: Auto-Joypad enabled. +;; NOTE: Does not read pads 0 & 1. Use `ScanPads` to read pads 0 & 1. +;; +;; REQUIRES: Joypad Auto-Read enabled. +;; REQUIRES: REG_WRIO bit 7 set BEFORE Vblank starts. ;; ;; ACCU 8 ;; INDEX 16 ;; DB = 0 ;; D = tcc__registers_irq (NOT ZERO) scanMPlay5_: - rep #$20 ; copy joy states #1->5 - ldy pad_keys - sty pad_keysold - ldy pad_keys+2 - sty pad_keysold+2 - ldy pad_keys+4 - sty pad_keysold+4 - ldy pad_keys+6 - sty pad_keysold+6 - ldy pad_keys+8 - sty pad_keysold+8 + ; Using the multitap reading protocol from the SNES Development Wiki + ; https://snes.nesdev.org/wiki/Multitap - sep #$20 +.function __pad_n(array, index) (array + index * 2) - lda #1 ; wait until joypads are ready --: bit REG_HVBJOY - bne - + ; Save old pad state + ; pads 0 & 1 are read by ScanPads. + ldy.w __pad_n(pad_keys, 2) + sty.w __pad_n(pad_keysold, 2) + + ldy.w __pad_n(pad_keys, 3) + sty.w __pad_n(pad_keysold, 3) + + ldy.w __pad_n(pad_keys, 4) + sty.w __pad_n(pad_keysold, 4) - lda.b #$80 ; enable iobit to read data - sta.w REG_WRIO - - lda.b #$1 - sta.w REG_JOYA ; do stobe on/off - stz.w REG_JOYA - - rep #$20 - ldy #16 -getpad1data: ; get all 16 bits pad1 data serialy - lda.w REG_JOYA - lsr a ; put bit0 into carry - rol.w pad_keys ; pad 1 data - dey - bne getpad1data - - ldy #16 -getpad23data: ; get all 16 bits pad2&3 data serialy - lda.w REG_JOYB - lsr a ; put bit1 into carry - rol.w pad_keys+2 ; pad 2 data - lsr a ; put bit1 into carry - rol.w pad_keys+4 ; pad 3 data - dey - bne getpad23data sep #$20 - stz.w REG_WRIO ; to allow read for other pads - - rep #$20 - ldy #16 -getpad45data: ; get all 16 bits pad2&3 data serialy - lda.w REG_JOYB - lsr a ; put bit1 into carry - rol.w pad_keys+6 ; pad 4 data - lsr a ; put bit1 into carry - rol.w pad_keys+8 ; pad 5 data - dey - bne getpad45data - - lda pad_keys - eor pad_keysold ; compute 'down' state from bits that - and pad_keys ; have changed from 0 to 1 - sta pad_keysrepeat ; - lda pad_keys+2 - eor pad_keysold+2 - and pad_keys+2 - sta pad_keysrepeat+2 - lda pad_keys+4 - eor pad_keysold+4 - and pad_keys+4 - sta pad_keysrepeat+4 - lda pad_keys+6 - eor pad_keysold+6 - and pad_keys+6 - sta pad_keysrepeat+6 - lda pad_keys+8 - eor pad_keysold+8 - and pad_keys+8 - sta pad_keysrepeat+8 +.ACCU 8 + + ; Wait until Joypad Auto-Read has finished + lda.b #1 +- + bit.w REG_HVBJOY + bne - + + ; Pads 3 & 4 must be manually read using the REG_JOYB register. + ; + ; The strobe/latch pin does not need to be toggled. + ; All 4 controllers on the mutlitap share the latch pin. + + ; Switch multitap to the second pair of controllers + stz.w REG_WRIO + + ; Initialise pad_keys to 1 so a `rol` outputs carry set after 8 `rol` instructions + ; A = 1 + sta.w __pad_n(pad_keys, 4) + sta.w __pad_n(pad_keys, 4) + 1 + + ; Read high byte of pads 3/4 + - + lda.w REG_JOYB + lsr + rol.w __pad_n(pad_keys, 3) + 1 + lsr + rol.w __pad_n(pad_keys, 4) + 1 + bcc - + + ; Read low byte of pads 3/4 + - + lda.w REG_JOYB + lsr + rol.w __pad_n(pad_keys, 3) + lsr + rol.w __pad_n(pad_keys, 4) + bcc - + + rep #$30 +.ACCU 16 +.INDEX 16 + + ; Pads 0 & 1 are read by ScanPads. + + ; Read & process pad 2 from Auto-Joy + lda.w REG_JOY4L + bit.w #$0f + beq + + ; Not a standard controller + lda.w #0 + + + sta.w __pad_n(pad_keys, 2) + eor.w __pad_n(pad_keysold, 2) + and.w __pad_n(pad_keys, 2) + sta.w __pad_n(pad_keysrepeat, 2) + + ; Process pads 3 & 4 + .REPEAT 2 INDEX _I + .REDEFINE @p = 3 + _I + .ASSERT @p < 5 + + lda.w __pad_n(pad_keys, @p) + bit.w #$0f + beq + + ; Not a standard controller + lda.w #0 + sta.w __pad_n(pad_keys, @p) + + + eor.w __pad_n(pad_keysold, @p) + and.w __pad_n(pad_keys, @p) + sta.w __pad_n(pad_keysrepeat, @p) + .ENDR + sep #$20 - lda.b #$80 ; enable iobit for next frame - sta.w REG_WRIO +.ACCU 8 + + ; Switch multitap back to the first pair of controllers + ; Ensures Auto-Joy will read pads 2/3 on the next VBlank. + lda.b #$80 + sta.w REG_WRIO rtl .ENDS @@ -554,6 +576,7 @@ FVBlank: lda snes_mplay5 beq + jsl scanMPlay5_ + jsl scanPads_ bra @EndScanPads + lda snes_mouse From 080ae02f0117c07ea318f58d553661c3f963d5d9 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Thu, 16 May 2024 20:43:48 +1000 Subject: [PATCH 070/106] Use local labels and child labels in the VBlank ISR --- pvsneslib/source/vblank.asm | 104 ++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index f03f1bc5..81c1e3c6 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -35,8 +35,9 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose .ENDS -.BASE BASE_0 -.SECTION ".pads0_text" SUPERFREE +; Needed to satisfy interrupt definition in "Header.inc". +.SECTION ".vblank_isr" SEMIFREE ORG ORG_0 BANK 0 + ;; Scan and read the joypads ;; @@ -47,7 +48,7 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose ;; INDEX 16 ;; DB = 0 ;; D = tcc__registers_irq (NOT ZERO) -scanPads_: +_scanPads: ldy pad_keys ; copy joy states #1&2 sty pad_keysold ldy pad_keys+2 @@ -81,10 +82,9 @@ scanPads_: sep #$20 rtl -.ENDS -.SECTION ".padsm51_text" SUPERFREE +;--------------------------------------------------------------------------------- ;; Scan and read the last 3 controllers on a Multitap or MP5 connected to Port 2. @@ -98,7 +98,7 @@ scanPads_: ;; INDEX 16 ;; DB = 0 ;; D = tcc__registers_irq (NOT ZERO) -scanMPlay5_: +_scanMPlay5: ; Using the multitap reading protocol from the SNES Development Wiki ; https://snes.nesdev.org/wiki/Multitap @@ -201,10 +201,8 @@ scanMPlay5_: sta.w REG_WRIO rtl -.ENDS -.SECTION ".padsscop_text" SEMIFREE ORG ORG_0 BANK 0 ;--------------------------------------------------------------------------------- ; Nintendo SHVC Scope BIOS version 1.00 @@ -233,13 +231,13 @@ scanMPlay5_: ; 3c: at some point, set "CenterH"/"CenterV" equal to "ShotHRaw"/"ShotVRaw" ; so that the aim-adjusted coordinates are "correct" ;--------------------------------------------------------------------------------- -GetScope_: +_GetScope: php sep #$20 lda REG_STAT78 ; Has the PPU counter been latched? and.b #$40 ; If not, don't get the scanline location - beq NoShot + beq @NoShot lda REG_OPHCT ; Get the horizontal scanline location (bits 0-7) sta scope_shoth @@ -271,13 +269,13 @@ GetScope_: sta scope_shotv stz scope_sinceshot ; update number of frames since last shot - bra GetInput ; (what happens if 65536 frames elapse between shots?) + bra @GetInput ; (what happens if 65536 frames elapse between shots?) -NoShot: +@NoShot: inc scope_sinceshot ; Wait for valid joypad input -GetInput: +@GetInput: sep #$20 -: lda REG_HVBJOY @@ -296,10 +294,10 @@ GetInput: lda scope_port2down ; Check if the controller in port 2 is a Super Scope. and.w #$0CFF ; For a 16-bit auto joypad read, bits 0-7 should be always 1 cmp.w #$00FF ; and bits 10-11 should be always 0. - bne NoScope + bne @NoScope lda scope_sinceshot ; has a shot already happened this frame? - beq GetButtons ; If so, then only pay attention to the pause button bit + beq @GetButtons ; If so, then only pay attention to the pause button bit lda scope_port2down ; Check which already-held buttons are still held and scope_last @@ -316,7 +314,7 @@ GetInput: plp ; return from input check rts -GetButtons: +@GetButtons: lda scope_port2down ; Get button status when NOT paused sta scope_down eor scope_last @@ -325,33 +323,33 @@ GetButtons: sta scope_held lda scope_port2down ; if no bits are set on port 2, don't check for "held buttons". - beq NotHolding + beq @NotHolding cmp scope_last ; else if the bits aren't the same as last frame, don't check either. - bne NotHolding + bne @NotHolding dec scope_tohold ; if a certain number of frames have elapsed with the same buttons - bne NotHeld ; held down, consider them "officially held". + bne @NotHeld ; held down, consider them "officially held". lda scope_port2down sta scope_held lda scope_repdelay ; set the remaining delay to the repeat value sta scope_tohold - bra NotHeld + bra @NotHeld -NotHolding: +@NotHolding: lda scope_holddelay ; set the remaining delay to the normal value sta scope_tohold -NotHeld: +@NotHeld: lda scope_port2down sta scope_last plp ; return from input check rts -NoScope: +@NoScope: stz scope_port2down ; If no scope is connected, zero out all inputs stz scope_port2now stz scope_down @@ -362,7 +360,7 @@ NoScope: plp ; return from input check rts -.ENDS + ;--------------------------------------------------------------------------------- @@ -390,8 +388,6 @@ NoScope: ;--------------------------------------------------------------------------------- -.SECTION ".mouse_text" SUPERFREE - ;; Read the mouse values ;; ;; REQUIRES: Auto-Joypad enabled. @@ -400,36 +396,36 @@ NoScope: ;; INDEX 16 ;; DB = 0 ;; D = tcc__registers_irq (NOT ZERO) -mouseRead_: +_mouseRead: sep #$30 -_10: +@_10: lda REG_HVBJOY and #$01 - bne _10 ; Automatic read ok? + bne @_10 ; Automatic read ok? ldx #$01 lda REG_JOY2L ; Joy2 - jsr mouse_data + jsr _mouse_data lda connect_st+1 - beq _20 + beq @_20 jsr speed_change stz connect_st+1 -_20: +@_20: dex lda REG_JOY1L ; Joy1 - jsr mouse_data + jsr _mouse_data lda connect_st - beq _30 + beq @_30 jsr speed_change stz connect_st -_30: +@_30: lda mouseConnect ora mouseConnect+1 @@ -443,12 +439,12 @@ _30: .accu 8 .index 8 -mouse_data: +_mouse_data: sta tcc__r0 ; (421A / 4218 saved to reg0) and.b #$0F cmp.b #$01 ; Is the mouse connected? - beq _m10 + beq @_m10 stz mouseConnect,x ; No connection. @@ -459,28 +455,28 @@ mouse_data: rts -_m10: +@_m10: lda mouseConnect,x ; When mouse is connected, speed will change. - bne _m20 ; Previous connection status + bne @_m20 ; Previous connection status ; (mouse.com judged by lower 1 bit) lda #$01 ; Connection check flag on sta mouseConnect,x sta connect_st,x rts -_m20: +@_m20: rep #$10 ldy #16 ; Read 16 bit data. sep #$10 -_m30: +@_m30: lda REG_JOYA,x lsr a rol mouse_x,x rol mouse_y,x dey - bne _m30 + bne @_m30 stz mousePressed,x @@ -491,29 +487,30 @@ _m30: lda mousePressed,x eor mouse_sb,x ; Get switch trigger - bne _m40 + bne @_m40 stz mouseButton,x rts -_m40: +@_m40: lda mousePressed,x sta mouseButton,x sta mouse_sb,x rts -.ENDS -; Needed to satisfy interrupt definition in "Header.inc". -.SECTION ".vblank" SEMIFREE ORG ORG_0 BANK 0 +;--------------------------------------------------------------------------------- .accu 16 .index 16 .16bit + +;; Vertical Blank Interrupt Service Routine (NMI ISR) +;; VBlank: .ifdef FASTROM jml FVBlank @@ -575,21 +572,21 @@ FVBlank: lda snes_mplay5 beq + - jsl scanMPlay5_ - jsl scanPads_ + jsl _scanMPlay5 + jsl _scanPads bra @EndScanPads + lda snes_mouse beq + - jsl mouseRead_ + jsl _mouseRead lda mouseConnect and mouseConnect + 1 ; If both ports have a mouse plugged, it will skip pad controller reading bne @EndScanPads + - jsl scanPads_ + jsl _scanPads lda snes_sscope beq @EndScanPads - jsr GetScope_ + jsr _GetScope @EndScanPads: rep #$30 @@ -603,6 +600,9 @@ FVBlank: .ENDS +;--------------------------------------------------------------------------- + + .BASE BASE_0 .SECTION ".waitforvblank_text" SUPERFREE From 3be8f8b3b91e0d24ff96b263016d76aef7415447 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Thu, 16 May 2024 21:20:16 +1000 Subject: [PATCH 071/106] Inline ScanPads and ScanMPlay5 in the VBlank ISR --- pvsneslib/source/vblank.asm | 67 +++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 81c1e3c6..434dfcfa 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -48,7 +48,7 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose ;; INDEX 16 ;; DB = 0 ;; D = tcc__registers_irq (NOT ZERO) -_scanPads: +.MACRO _ScanPads ldy pad_keys ; copy joy states #1&2 sty pad_keysold ldy pad_keys+2 @@ -81,7 +81,7 @@ _scanPads: sta pad_keysrepeat+2 ; sep #$20 - rtl +.ENDM ;--------------------------------------------------------------------------------- @@ -98,7 +98,7 @@ _scanPads: ;; INDEX 16 ;; DB = 0 ;; D = tcc__registers_irq (NOT ZERO) -_scanMPlay5: +.MACRO _ScanMPlay5 ; Using the multitap reading protocol from the SNES Development Wiki ; https://snes.nesdev.org/wiki/Multitap @@ -199,8 +199,7 @@ _scanMPlay5: ; Ensures Auto-Joy will read pads 2/3 on the next VBlank. lda.b #$80 sta.w REG_WRIO - - rtl +.ENDM @@ -561,32 +560,38 @@ FVBlank: jsl tcc__jsl_r10 - ; Refresh pad values - sep #$20 + ; Refresh pad values + sep #$20 - ; Set Data Bank to access joypad registers and lowram - lda.b #0 - pha - plb + ; Set Data Bank to access joypad registers and lowram + lda.b #0 + pha + plb ; DB = 0 - lda snes_mplay5 - beq + - jsl _scanMPlay5 - jsl _scanPads - bra @EndScanPads -+ - lda snes_mouse - beq + - jsl _mouseRead - lda mouseConnect - and mouseConnect + 1 ; If both ports have a mouse plugged, it will skip pad controller reading - bne @EndScanPads -+ - jsl _scanPads - lda snes_sscope - beq @EndScanPads - jsr _GetScope + + lda snes_mplay5 + bne @ScanMp5 + + lda snes_mouse + beq + + jsl _mouseRead + + ; If both ports have a mouse plugged, it will skip pad controller reading + lda mouseConnect + and mouseConnect + 1 + bne @EndScanPads + + + + lda snes_sscope + beq + + jsr _GetScope + bra @EndScanPads + + + +@ScanPads: + _ScanPads + @EndScanPads: rep #$30 @@ -598,6 +603,12 @@ FVBlank: plb RTI + + +@ScanMp5: + _ScanMPlay5 + jmp @ScanPads + .ENDS ;--------------------------------------------------------------------------- From 191357972a090527feba4e3f99dc9f3ab6829689 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Fri, 17 May 2024 19:08:14 +1000 Subject: [PATCH 072/106] Only test if Joypad Auto-Read has finished once in the VBlank ISR This is safe as the input reading subroutines/macros are private to the vblank_isr section --- pvsneslib/source/vblank.asm | 56 ++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 434dfcfa..15a48193 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -44,6 +44,9 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose ;; REQUIRES: Auto-Joypad enabled. ;; REQUIRES: REG_WRIO bit 7 set BEFORE Vblank starts if a Multitap/Mp5 is connected. ;; +;; REQUIRES: Auto-Joypad has finished reading the controllers. +;; (This macro DOES NOT contain a REG_HVBJOY test/spinloop.) +;; ;; ACCU 8 ;; INDEX 16 ;; DB = 0 @@ -54,9 +57,8 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose ldy pad_keys+2 sty pad_keysold+2 - lda #1 ; wait until joypads are ready --: bit REG_HVBJOY - bne - + ; The code assumes Joypad Auto-Read is not active. + ; This is enforced by a REG_HVBJOY spinloop in the VBlank ISR. rep #$20 @@ -94,6 +96,9 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose ;; REQUIRES: Joypad Auto-Read enabled. ;; REQUIRES: REG_WRIO bit 7 set BEFORE Vblank starts. ;; +;; REQUIRES: Auto-Joypad has finished reading the controllers. +;; (This macro DOES NOT contain a REG_HVBJOY test/spinloop.) +;; ;; ACCU 8 ;; INDEX 16 ;; DB = 0 @@ -119,11 +124,9 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose sep #$20 .ACCU 8 - ; Wait until Joypad Auto-Read has finished - lda.b #1 -- - bit.w REG_HVBJOY - bne - + ; The code assumes Joypad Auto-Read is not active. + ; This is enforced by a REG_HVBJOY spinloop in the VBlank ISR. + ; Pads 3 & 4 must be manually read using the REG_JOYB register. ; @@ -230,6 +233,17 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose ; 3c: at some point, set "CenterH"/"CenterV" equal to "ShotHRaw"/"ShotVRaw" ; so that the aim-adjusted coordinates are "correct" ;--------------------------------------------------------------------------------- + +;; Read the SuperScope from controller port 2 and PPU counter. +;; +;; REQUIRES: Auto-Joypad enabled. +;; REQUIRES: Auto-Joypad has finished reading the controllers. +;; (This subroutine DOES NOT contain a REG_HVBJOY test/spinloop.) +;; +;; DB = 0 +;; D = tcc__registers_irq (NOT ZERO) +.ACCU 8 +.INDEX 16 _GetScope: php sep #$20 @@ -275,11 +289,8 @@ _GetScope: ; Wait for valid joypad input @GetInput: - sep #$20 - --: lda REG_HVBJOY - and.b #$01 - bne - + ; The code assumes Joypad Auto-Read is not active. + ; This is enforced by a REG_HVBJOY spinloop in the VBlank ISR. rep #$20 lda REG_JOY2L ; Get joypad 2 input @@ -390,18 +401,18 @@ _GetScope: ;; Read the mouse values ;; ;; REQUIRES: Auto-Joypad enabled. +;; REQUIRES: Auto-Joypad has finished reading the controllers. +;; (This subroutine DOES NOT contain a REG_HVBJOY test/spinloop.) ;; -;; ACCU 8 -;; INDEX 16 ;; DB = 0 ;; D = tcc__registers_irq (NOT ZERO) +.ACCU 8 +.INDEX 16 _mouseRead: sep #$30 -@_10: - lda REG_HVBJOY - and #$01 - bne @_10 ; Automatic read ok? + ; The code assumes Joypad Auto-Read is not active. + ; This is enforced by a REG_HVBJOY spinloop in the VBlank ISR. ldx #$01 lda REG_JOY2L ; Joy2 @@ -569,6 +580,13 @@ FVBlank: plb ; DB = 0 + ; Wait until the Joypad Auto-Read has finished. + ; (done here so HVBJOY is only tested in one spot) + lda.b #1 + - + bit.w REG_HVBJOY + bne - + lda snes_mplay5 bne @ScanMp5 From 431c8c43749ec2ec7c2485fd2cac532619d5bfc3 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Fri, 17 May 2024 19:47:49 +1000 Subject: [PATCH 073/106] Optimise VBlank ISR OAM transfer --- pvsneslib/source/vblank.asm | 48 +++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 15a48193..e609f9bc 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -533,31 +533,39 @@ FVBlank: phx phy pha - ; set data bank register to bss section - pea $7e7e - plb - plb + pea $7e80 + plb +// DB = $80 + + ; Using 16 bit A so the `stz $2102` below clears a 16-bit register. + sep #$10 +.ACCU 16 +.INDEX 8 + + ; Transfer oamMemory to OAM + stz.w $2102 ; OAM address (word register) - ; Put oam to screen if needed - rep #$20 ; A 16 bits - lda.w #$0000 - sta.l $2102 ; OAM address - lda.w #$0400 - sta.l $4370 ; DMA type CPU -> PPU, auto inc, $2104 (OAM write) - lda.w #$0220 - sta.l $4375 ; DMA size (220 = 128*4+32 + lda.w #$0400 + sta.w $4370 ; DMA type CPU -> PPU, auto inc, $2104 (OAM write) - lda #oamMemory.w - sta.l $4372 ; DMA address = oam memory - sep #$20 - lda #:oamMemory - sta.l $4374 ; DMA address bank = oam memory + lda.w #$0220 + sta.w $4375 ; DMA size (0x220 = 128*4+32) - lda.b #$80 ; DMA channel 7 1xxx xxxx - sta.l $420b + lda.w #oamMemory.w + sta.w $4372 ; DMA address = oam memory - rep #$20 + ldx.b #:oamMemory + stx.w $4374 ; DMA address bank = oam memory + + ldx.b #$80 + stx.w $420b ; DMA channel 7 1xxx xxxx + + rep #$30 +.ACCU 16 +.INDEX 16 + plb +// DB = $7e ; Count frame number inc.w snes_vblank_count From 8ca97da25d64adda47fef8d14f6d6c4e5cb46829 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Fri, 17 May 2024 20:48:34 +1000 Subject: [PATCH 074/106] Add lag detection to the VBlank ISR During a lag frame: * oamMemory will not be transfered to the PPU OAM * the pads/mouse/scope will not be read nmi_handler will be called on every VBlank interrupt. The vblank_flag variable can be used to detect lag-frames in the nmi_handler. --- pvsneslib/include/snes/interrupt.h | 16 ++++ pvsneslib/source/crt0_snes.asm | 4 + pvsneslib/source/vblank.asm | 125 +++++++++++++++++------------ 3 files changed, 94 insertions(+), 51 deletions(-) diff --git a/pvsneslib/include/snes/interrupt.h b/pvsneslib/include/snes/interrupt.h index 9ee477b0..8e7bd919 100644 --- a/pvsneslib/include/snes/interrupt.h +++ b/pvsneslib/include/snes/interrupt.h @@ -36,6 +36,22 @@ #include +/** + * \brief VBlank ISR flag. + * + * Used to detect lag-frames in the VBlank ISR. + * + * This variable is set to a truthy (non-zero) value in WaitForVBlank() + * and cleared in the NMI ISR after the call to nmi_handler. + * + * vblank_flag can be used in a custom nmi_handler to detect lag frames. Within nmi_handler: + * - If vblank_flag is truthy (non-zero), the nmi_handler was called at the end of the frame. + * - If vblank_flag is 0, the nmi_handler was called in the middle of a lag frame. + * + * \b CAUTION: This variable SHOULD NOT be changed outside of WaitForVBlank() + */ +extern u8 vblank_flag; + extern void *nmi_handler; /** \brief VBlank NMI Enable (0=Disable, 1=Enable) (Initially disabled on reset) */ diff --git a/pvsneslib/source/crt0_snes.asm b/pvsneslib/source/crt0_snes.asm index ead5ea3c..d217d14a 100644 --- a/pvsneslib/source/crt0_snes.asm +++ b/pvsneslib/source/crt0_snes.asm @@ -235,6 +235,10 @@ fast_start: jsr tcc__snesinit + sep #$20 + + stz.w vblank_flag + rep #$30 ; all registers 16-bit ; direct page points to register set diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index e609f9bc..3e8065e5 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -23,7 +23,9 @@ ;--------------------------------------------------------------------------------- -.RAMSECTION ".registers" BANK 0 SLOT 1 PRIORITY 1 +.RAMSECTION ".vblank_bss" BANK 0 SLOT 1 PRIORITY 1 + +vblank_flag dsb 1 nmi_handler dsb 4 @@ -543,6 +545,8 @@ FVBlank: .ACCU 16 .INDEX 8 + ldx.w vblank_flag + beq + ; Transfer oamMemory to OAM stz.w $2102 ; OAM address (word register) @@ -560,6 +564,7 @@ FVBlank: ldx.b #$80 stx.w $420b ; DMA channel 7 1xxx xxxx + + rep #$30 .ACCU 16 @@ -567,9 +572,6 @@ FVBlank: plb // DB = $7e - ; Count frame number - inc.w snes_vblank_count - lda.w #tcc__registers_irq tad lda.l nmi_handler @@ -578,6 +580,11 @@ FVBlank: sta.b tcc__r10h jsl tcc__jsl_r10 + ; This marks the end of the Vertical Blanking Period critical code + + + ; Count frame number + inc.w snes_vblank_count ; Refresh pad values sep #$20 @@ -586,39 +593,49 @@ FVBlank: lda.b #0 pha plb -; DB = 0 + ; DB = 0 - ; Wait until the Joypad Auto-Read has finished. - ; (done here so HVBJOY is only tested in one spot) - lda.b #1 - - - bit.w REG_HVBJOY - bne - + lda.w vblank_flag + bne @ReadInputs + ; The VBlank interrupt occurred in a lag frame + ; Do not read inputs. + jmp @EndReadInputs - lda snes_mplay5 - bne @ScanMp5 + @ReadInputs: + ; Not in a lag frame + ; Read inputs - lda snes_mouse - beq + - jsl _mouseRead + stz.w vblank_flag - ; If both ports have a mouse plugged, it will skip pad controller reading - lda mouseConnect - and mouseConnect + 1 - bne @EndScanPads - + + ; Wait until the Joypad Auto-Read has finished. + ; (done here so HVBJOY is only tested in one spot) + lda.b #1 + - + bit.w REG_HVBJOY + bne - - lda snes_sscope - beq + - jsr _GetScope - bra @EndScanPads - + + lda snes_mplay5 + bne @ScanMp5 + + lda snes_mouse + beq + + jsl _mouseRead -@ScanPads: - _ScanPads + ; If both ports have a mouse plugged, it will skip pad controller reading + lda.w mouseConnect + and.w mouseConnect + 1 + bne @EndReadInputs + + + lda snes_sscope + beq + + jsr _GetScope + bra @EndReadInputs + + + @ScanPads: + _ScanPads -@EndScanPads: +@EndReadInputs: rep #$30 @@ -646,22 +663,24 @@ FVBlank: ;--------------------------------------------------------------------------- WaitForVBlank: - wai - rtl + php + sep #$20 + + ; TODO: is this required? + pha +.ACCU 8 -; old version still here for memory purpose -; pha -; php -; sep #$20 -;-: -; lda.l REG_RDNMI -; bmi - -;-: -; lda.l REG_RDNMI -; bpl - -; plp -; pla -; rtl + lda #1 + sta.w vblank_flag + + - + wai + lda.w vblank_flag + bne - + + pla + plp + rtl .ENDS @@ -671,15 +690,19 @@ WaitForVBlank: ;--------------------------------------------------------------------------- ; void WaitNVBlank(u16 ntime) WaitNVBlank: - php + php - sep #$20 - lda 5,s -- wai - dea - bne - + sep #$20 + + lda 5,s + - + pha + jsl WaitForVBlank + pla + dea + bne - - plp + plp rtl .ENDS From 38fc171549c061671349de27fbc92ae7b6acf075 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Fri, 17 May 2024 21:30:13 +1000 Subject: [PATCH 075/106] Add lag_frame_counter --- pvsneslib/include/snes/interrupt.h | 15 +++++++++++++++ pvsneslib/source/crt0_snes.asm | 2 ++ pvsneslib/source/vblank.asm | 11 ++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pvsneslib/include/snes/interrupt.h b/pvsneslib/include/snes/interrupt.h index 8e7bd919..28ab22a3 100644 --- a/pvsneslib/include/snes/interrupt.h +++ b/pvsneslib/include/snes/interrupt.h @@ -52,6 +52,21 @@ */ extern u8 vblank_flag; +/** + * \brief Lag-frame counter. + * + * This variable is incremented on every VBlank Interrupt that occurs during a lag-frame. + * + * \b CAUTION: The lag frame counter cannot tell the difference between a lag-frame and + * force-blank setup loading graphics to the PPU. + * + * \c lag_frame_counter can be modified. This is useful in development builds to measure the + * amount of lag in a level by resetting \c lag_frame_counter on level load and printing + * \c lag_frame_counter on a pause screen or at the end of the level. + */ +extern u16 lag_frame_counter; + + extern void *nmi_handler; /** \brief VBlank NMI Enable (0=Disable, 1=Enable) (Initially disabled on reset) */ diff --git a/pvsneslib/source/crt0_snes.asm b/pvsneslib/source/crt0_snes.asm index d217d14a..e002e58b 100644 --- a/pvsneslib/source/crt0_snes.asm +++ b/pvsneslib/source/crt0_snes.asm @@ -297,6 +297,8 @@ fast_start: stz.b tcc__r0 stz.b tcc__r1 + stz.w lag_frame_counter + stz.w snes_vblank_count stz.w snes_vblank_count_svg stz.w snes_frame_count diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 3e8065e5..16975cf2 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -29,6 +29,8 @@ vblank_flag dsb 1 nmi_handler dsb 4 +lag_frame_counter dsb 2 ; Number of lag frames encountered (can be externally modified) + snes_vblank_count dsb 2 ; 2 bytes to count number of vblank snes_vblank_count_svg dsb 2 ; same thing for saving purpose snes_frame_count dsb 2 ; 2 bytes for frame counter inside loop @@ -600,7 +602,14 @@ FVBlank: ; The VBlank interrupt occurred in a lag frame ; Do not read inputs. - jmp @EndReadInputs + ldx.w lag_frame_counter + inx + bne + + dex + + + stx.w lag_frame_counter + + jmp @EndReadInputs @ReadInputs: ; Not in a lag frame From 693390255841cb2e18cdacdd589f72c7d45e7f2f Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Fri, 17 May 2024 21:46:53 +1000 Subject: [PATCH 076/106] Optimise nmi_handler call --- pvsneslib/source/vblank.asm | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 16975cf2..81bd64d3 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -574,13 +574,14 @@ FVBlank: plb // DB = $7e - lda.w #tcc__registers_irq - tad - lda.l nmi_handler - sta.b tcc__r10 - lda.l nmi_handler + 2 - sta.b tcc__r10h - jsl tcc__jsl_r10 + ; Change Direct Page Register to prevent the `nmi_handler` call + ; from clobbering tcc imaginary registers. + lda.w #tcc__registers_irq + tad +// D = tcc__registers_irq + + jsl __JumpTo_nmi_handler + ; This marks the end of the Vertical Blanking Period critical code @@ -661,6 +662,17 @@ FVBlank: _ScanMPlay5 jmp @ScanPads + +;; Long jump to `nmi_handler` +;; +;; ACCU 16 +;; INDEX 16 +;; DB = $7e +;; D = tcc__registers_irq +__JumpTo_nmi_handler: + ; The `JML [addr]` instruction will always read the new program counter from Bank 0 + jml [nmi_handler] + .ENDS ;--------------------------------------------------------------------------- From 4de15010048cffad886fad731f21ffc06ffedd23 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sat, 18 May 2024 10:08:05 +1000 Subject: [PATCH 077/106] Fix crash when using HIROM mapping Crash was caused by a missing `.BASE 0` in the `.vblank_bss` ramsection. This commit also fixes 2 more sections with the wrong .BASE. --- pvsneslib/source/input.asm | 1 + pvsneslib/source/vblank.asm | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pvsneslib/source/input.asm b/pvsneslib/source/input.asm index ca71356d..b8d28f96 100644 --- a/pvsneslib/source/input.asm +++ b/pvsneslib/source/input.asm @@ -121,6 +121,7 @@ connect_st dsb 2 +.BASE BASE_0 .SECTION ".pads1_text" SUPERFREE ;--------------------------------------------------------------------------------- diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 81bd64d3..61e85041 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -23,6 +23,7 @@ ;--------------------------------------------------------------------------------- +.BASE $00 .RAMSECTION ".vblank_bss" BANK 0 SLOT 1 PRIORITY 1 vblank_flag dsb 1 @@ -39,8 +40,9 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose .ENDS -; Needed to satisfy interrupt definition in "Header.inc". -.SECTION ".vblank_isr" SEMIFREE ORG ORG_0 BANK 0 +; Needed to ensure VBlank ISR is in bank zero so the 65816 can access it when a NMI interrupt occurs. +.BASE BASE_0 +.SECTION ".vblank_isr" SEMIFREE ORGA $8000 BASE BASE_0 BANK 0 ;; Scan and read the joypads From adc0cd2316d6f9fffc311b2893e319ebc4990ee8 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sat, 18 May 2024 10:16:32 +1000 Subject: [PATCH 078/106] Cleanup vblank.asm indentation and alignment --- pvsneslib/source/vblank.asm | 44 +++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 61e85041..f371e7ea 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -26,16 +26,16 @@ .BASE $00 .RAMSECTION ".vblank_bss" BANK 0 SLOT 1 PRIORITY 1 -vblank_flag dsb 1 +vblank_flag dsb 1 -nmi_handler dsb 4 +nmi_handler dsb 4 lag_frame_counter dsb 2 ; Number of lag frames encountered (can be externally modified) -snes_vblank_count dsb 2 ; 2 bytes to count number of vblank -snes_vblank_count_svg dsb 2 ; same thing for saving purpose -snes_frame_count dsb 2 ; 2 bytes for frame counter inside loop -snes_frame_count_svg dsb 2 ; same thing for saving purpose +snes_vblank_count dsb 2 ; 2 bytes to count number of vblank +snes_vblank_count_svg dsb 2 ; same thing for saving purpose +snes_frame_count dsb 2 ; 2 bytes for frame counter inside loop +snes_frame_count_svg dsb 2 ; same thing for saving purpose .ENDS @@ -533,12 +533,16 @@ VBlank: FVBlank: .endif - rep #$30 - phb - phd - phx - phy - pha + + rep #$30 + + ; Push CPU registers to the stack. + ; A/X/Y must be saved in 16-bit mode (**before** switching to an 8 bit index) + phb + phd + phx + phy + pha pea $7e80 plb @@ -649,14 +653,16 @@ FVBlank: @EndReadInputs: - rep #$30 + ; Restore CPU registers + rep #$30 + + pla + ply + plx + pld + plb - pla - ply - plx - pld - plb - RTI + rti From f9ca99fba5cd15253147f9082d4ac043e9538a0a Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sat, 18 May 2024 10:20:26 +1000 Subject: [PATCH 079/106] Fix decimal mode flag not cleared in VBlank ISR --- pvsneslib/source/vblank.asm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index f371e7ea..b381fc0b 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -534,7 +534,7 @@ VBlank: FVBlank: .endif - rep #$30 + rep #$38 ; 16 bit A, 16 bit I, decimal mode disabled ; Push CPU registers to the stack. ; A/X/Y must be saved in 16-bit mode (**before** switching to an 8 bit index) From 6ee90751ec42a6ad35e9edea432040b5d38aa140 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sat, 18 May 2024 10:23:08 +1000 Subject: [PATCH 080/106] Set the Direct Page register at the start of the VBlank ISR --- pvsneslib/source/vblank.asm | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index b381fc0b..c10abc53 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -544,9 +544,17 @@ FVBlank: phy pha + ; Direct Page and DB are unknown. Set them here. + + ; Using a different Direct Page Register value to prevent the `nmi_handler` call + ; from clobbering tcc imaginary registers. + lda.w #tcc__registers_irq + tad +; D = tcc__registers_irq + pea $7e80 plb -// DB = $80 +; DB = $80 ; Using 16 bit A so the `stz $2102` below clears a 16-bit register. sep #$10 @@ -578,13 +586,7 @@ FVBlank: .ACCU 16 .INDEX 16 plb -// DB = $7e - - ; Change Direct Page Register to prevent the `nmi_handler` call - ; from clobbering tcc imaginary registers. - lda.w #tcc__registers_irq - tad -// D = tcc__registers_irq +; DB = $7e jsl __JumpTo_nmi_handler From eb80472c1e777280a63a149e2be01dd268a9b33f Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sat, 18 May 2024 10:26:37 +1000 Subject: [PATCH 081/106] Fix pad_keys* array size in input.h --- pvsneslib/include/snes/input.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvsneslib/include/snes/input.h b/pvsneslib/include/snes/input.h index 25a2e7f3..64f9a5d4 100644 --- a/pvsneslib/include/snes/input.h +++ b/pvsneslib/include/snes/input.h @@ -73,9 +73,9 @@ typedef enum SUPERSCOPE_BITS SSC_NOISE = BIT(8), //!< superscope NOISE flag. } SUPERSCOPE_BITS; -extern u16 pad_keys[2]; -extern u16 pad_keysold[2]; -extern u16 pad_keysrepeat[2]; +extern u16 pad_keys[5]; +extern u16 pad_keysold[5]; +extern u16 pad_keysrepeat[5]; extern u8 snes_mplay5; /*! \brief 1 if MultiPlay5 is connected */ extern u8 snes_mouse; /*! \brief 1 if Mouse is going to be used */ From 33ac17f6a8ce560de501e7949058e2a110822b12 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sat, 18 May 2024 11:03:11 +1000 Subject: [PATCH 082/106] Rename vblank ram section to .vblank_lowram --- pvsneslib/source/vblank.asm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index c10abc53..af343f91 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -23,8 +23,9 @@ ;--------------------------------------------------------------------------------- +; Using lowram segment (Work-RAM address $0000-$1FFF) for VBlank variables so they can be accessed with a DB of $00. .BASE $00 -.RAMSECTION ".vblank_bss" BANK 0 SLOT 1 PRIORITY 1 +.RAMSECTION ".vblank_lowram" BANK 0 SLOT 1 PRIORITY 1 vblank_flag dsb 1 From 7eb88c28bc2641b7ea7069559c08ba8c430d29ff Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sat, 18 May 2024 06:41:55 +0200 Subject: [PATCH 083/106] fix(size): put tiles in 256 pix width --- tools/tilesetextractor/tileset-extractor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/tilesetextractor/tileset-extractor.js b/tools/tilesetextractor/tileset-extractor.js index 5681060c..b2a1ba1b 100644 --- a/tools/tilesetextractor/tileset-extractor.js +++ b/tools/tilesetextractor/tileset-extractor.js @@ -259,8 +259,8 @@ function onLoad() { // Try to get as squared as possible // const numTiles = tiles.length; - const numRows = Math.sqrt(numTiles) | 0; - const numCols = Math.ceil(numTiles / numRows) | 0; + const numRows = Math.ceil((numTiles*tileWidth)/256) | 0; // Math.sqrt(numTiles) | 0; + const numCols = 256/tileWidth; // Math.ceil(numTiles / numRows) | 0; extractedTilesWidth = numCols * tileWidth; extractedTilesHeight = numRows * tileHeight; const canvas = document.createElement("canvas"); From 3fd2131a1f426f0762f369fe2ec070f7f7054e6a Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Sat, 18 May 2024 06:42:45 +0200 Subject: [PATCH 084/106] fix(fps): add text offset and optimize code --- pvsneslib/source/videos.asm | 62 +++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/pvsneslib/source/videos.asm b/pvsneslib/source/videos.asm index 0b35e1ed..0bf17034 100644 --- a/pvsneslib/source/videos.asm +++ b/pvsneslib/source/videos.asm @@ -1147,25 +1147,42 @@ showFPScounter: php phb - jsl getFPScounter ; compute fps - sep #$20 + lda #$0 ; bank 0 for counters + pha + plb + + rep #$20 + lda snes_vblank_count + cmp snes_vblank_count_svg ; is svg < current counter, exit to display (normaly, never occurs) + bcc _sfctr1 + sec + sbc.l snes_vblank_count_svg ; check if we reach fps (50 or 60) + sbc.l snes_fps + bcc _sfctr + lda snes_vblank_count ; save vblank count + sta snes_vblank_count_svg + + lda snes_frame_count_svg ; init again frame counter + cmp #99 ; no more than 99 fps (don't be mad ;) ) + bcc + + lda #99 + ++: sta snes_frame_count + stz snes_frame_count_svg +_sfctr: + inc.w snes_frame_count_svg ; increment current frame counter + +_sfctr1: + sep #$20 lda.l snes_frame_count sta.l $4204 lda.l snes_frame_count+1 ; Write $fps to dividend sta.l $4205 - LDA #10 ; Write 10 to divisor - sta.l $4206 - nop ; Wait 16 machine cycles - nop - nop - nop - nop - nop - nop - nop - + LDA #10 ; Write 10 to divisor (to have fps/10 for 1st char) + sta.l $4206 ; Wait 16 machine cycles after (done by code) + lda #$80 ; VRAM_INCHIGH | VRAM_ADRTR_0B | VRAM_ADRSTINC_1 set address in VRam for read or write ($2116) + block size transfer ($2115) sta.l $2115 rep #$20 @@ -1178,16 +1195,21 @@ showFPScounter: lda.l $4214 ; A = result low byte ($4215 result high byte) clc adc #$10 ; to have number 0 of graphic - sta.l $2118 - lda #$1 - sta.l $2119 + rep #$20 + and #$00FF + clc + adc txt_vram_offset ; add text offset and put 16 bit value to VRAM + sta.l $2118 - lda.l $4216 ; A = remainder low byte ($4216 remainder high byte) + sep #$20 + lda.l $4216 ; A = remainder low byte ($4216 remainder high byte) (so fps mod 10) clc adc #$10 ; to have number 0 of graphic - sta.l $2118 - lda #$1 - sta.l $2119 + rep #$20 + and #$00FF + clc + adc txt_vram_offset + sta.l $2118 ; add text offset and put 16 bit value to VRAM plb plp From 8787ec2ffe65e182f04b081520bfe8b8480256eb Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Thu, 23 May 2024 20:06:29 +1000 Subject: [PATCH 085/106] Fix a single frame of uninitialized OAM after setScreenOn() --- pvsneslib/include/snes/video.h | 2 ++ pvsneslib/source/videos.asm | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pvsneslib/include/snes/video.h b/pvsneslib/include/snes/video.h index d9b67a1d..a525e3a6 100644 --- a/pvsneslib/include/snes/video.h +++ b/pvsneslib/include/snes/video.h @@ -337,6 +337,8 @@ void setMode(u8 mode, u8 size); /*! \fn setScreenOn(void) \brief Put screen On. + + Calls WaitForVBlank() before enabling the screen to flush VBlank buffers/queues and minimise glitches. */ void setScreenOn(void); diff --git a/pvsneslib/source/videos.asm b/pvsneslib/source/videos.asm index 0b35e1ed..36ebefd6 100644 --- a/pvsneslib/source/videos.asm +++ b/pvsneslib/source/videos.asm @@ -282,9 +282,14 @@ setScreenOn: php sep #$20 - lda #$f - wai + ; Calling WaitForVBlank to: + ; * Flush any unsent VBlank ISR/routine buffers/queues (fixes an uninitialized OAM glitch). + ; * Ensure the input/pad state is up-to-date when `setScreenOn()` returns. + ; * Prevent screen tearing and a single frame glitch that occurs when the screen is enabled mid-frame. + jsl WaitForVBlank + + lda #$f sta.l REG_INIDISP plp From d450096471ec9f390d4a437b7958ec413bab9521 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Thu, 23 May 2024 20:16:16 +1000 Subject: [PATCH 086/106] Fix examples that loop infinitely instead of waiting for a key --- .../graphics/Effects/Fading/Fading.c | 22 ++++++++++----- .../Effects/MosaicShading/MosaicShading.c | 27 ++++++++++--------- snes-examples/random/random.c | 6 ++--- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/snes-examples/graphics/Effects/Fading/Fading.c b/snes-examples/graphics/Effects/Fading/Fading.c index fc615489..d37a6dc6 100644 --- a/snes-examples/graphics/Effects/Fading/Fading.c +++ b/snes-examples/graphics/Effects/Fading/Fading.c @@ -13,6 +13,15 @@ extern char palette, palette_end; extern char map, map_end; //--------------------------------------------------------------------------------- + +// NOTE: Does not pause execution if a pad 0 key is currently pressed. +void WaitForKey() { + while (padsCurrent(0) == 0) + { + WaitForVBlank(); + } +} + int main(void) { // Copy tiles to VRAM @@ -27,8 +36,7 @@ int main(void) bgSetDisable(2); setScreenOn(); - // Wait for a key - while (!padsCurrent(0)); + WaitForKey(); // Now just play with effects :P while (1) @@ -36,22 +44,22 @@ int main(void) // Fade out so light to black setFadeEffectEx(FADE_OUT,12); WaitForVBlank(); - while (!padsCurrent(0)); + WaitForKey(); // Fade in now so black to light setFadeEffectEx(FADE_IN,24); WaitForVBlank(); - while (!padsCurrent(0)); + WaitForKey(); // Fade out so light to black setFadeEffect(FADE_OUT); WaitForVBlank(); - while (!padsCurrent(0)); + WaitForKey(); // Fade in now so black to light setFadeEffect(FADE_IN); WaitForVBlank(); - while (!padsCurrent(0)); + WaitForKey(); } return 0; -} \ No newline at end of file +} diff --git a/snes-examples/graphics/Effects/MosaicShading/MosaicShading.c b/snes-examples/graphics/Effects/MosaicShading/MosaicShading.c index 43b5d07e..faea7de1 100644 --- a/snes-examples/graphics/Effects/MosaicShading/MosaicShading.c +++ b/snes-examples/graphics/Effects/MosaicShading/MosaicShading.c @@ -13,6 +13,15 @@ extern char palette, palette_end; extern char map, map_end; //--------------------------------------------------------------------------------- + +// NOTE: Does not pause execution if a pad 0 key is currently pressed. +void WaitForKey() { + while (padsCurrent(0) == 0) + { + WaitForVBlank(); + } +} + int main(void) { // Copy tiles to VRAM @@ -27,9 +36,7 @@ int main(void) bgSetDisable(2); setScreenOn(); - // Wait for a key - while (!padsCurrent(0)) - ; + WaitForKey(); // Now just play with effects :P while (1) @@ -37,26 +44,22 @@ int main(void) // Fade out so light to black setFadeEffect(FADE_OUT); WaitForVBlank(); - while (!padsCurrent(0)) - ; + WaitForKey(); // Fade in now so black to light setFadeEffect(FADE_IN); WaitForVBlank(); - while (!padsCurrent(0)) - ; + WaitForKey(); // Now do some big pixels setMosaicEffect(MOSAIC_OUT, MOSAIC_BG1); WaitForVBlank(); - while (!padsCurrent(0)) - ; + WaitForKey(); // And now restore screen to normal setMosaicEffect(MOSAIC_IN, MOSAIC_BG1); WaitForVBlank(); - while (!padsCurrent(0)) - ; + WaitForKey(); } return 0; -} \ No newline at end of file +} diff --git a/snes-examples/random/random.c b/snes-examples/random/random.c index 6cb86f76..1e2d8f36 100644 --- a/snes-examples/random/random.c +++ b/snes-examples/random/random.c @@ -39,11 +39,11 @@ int main(void) while (1) { - pad0 = padsCurrent(0); consoleDrawText(6, 12, "RANDOM NUMBER=%04x", rand() & 0xFFFF); - while (pad0 == 0) + WaitForVBlank(); + + while (padsCurrent(0) == 0) { - pad0 = padsCurrent(0); WaitForVBlank(); } } From 49ab25551de56739f9b3a8b689950dca314a4ab4 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Fri, 24 May 2024 19:22:57 +1000 Subject: [PATCH 087/106] Fix lag_frame_counter incrementing in fade/mosaic effects Modifies WaitForVBlank() so it can be called with any DB register value. Also documents that WaitForVBlank() will preserve A/X/Y registers. --- pvsneslib/include/snes/interrupt.h | 2 ++ pvsneslib/source/vblank.asm | 38 ++++++++++++++++++++++++++---- pvsneslib/source/videos.asm | 26 ++++++++++++-------- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/pvsneslib/include/snes/interrupt.h b/pvsneslib/include/snes/interrupt.h index 28ab22a3..10e9295a 100644 --- a/pvsneslib/include/snes/interrupt.h +++ b/pvsneslib/include/snes/interrupt.h @@ -185,6 +185,8 @@ The H/V-IRQ flag in Bit7 of TIMEUP, Port 4211h gets set when the V-Counter gets * Wait for vblank interrupt
* * Waits for a vertical blank interrupt
+ * + * Assembly note: This function will not modify the A/X/Y registers and can be called with an 8 or 16 bit .ACCU/.INDEX. */ void WaitForVBlank(void); diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index af343f91..ef4d725d 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -693,21 +693,29 @@ __JumpTo_nmi_handler: .BASE BASE_0 .SECTION ".waitforvblank_text" SUPERFREE -;--------------------------------------------------------------------------- +; WaitForVBlank(void); +; +; KEEP: A, X, Y (documented in interrupt.h) +; A unknown +; I unknown +; DB unknown +; D unknown WaitForVBlank: php sep #$20 +.ACCU 8 - ; TODO: is this required? pha -.ACCU 8 + + ; MUST NOT modify X, Y or high-byte of A + ; This behaviour is documented in interrupt.h and used by `video.asm`. lda #1 - sta.w vblank_flag + sta.l vblank_flag - wai - lda.w vblank_flag + lda.l vblank_flag bne - pla @@ -717,6 +725,26 @@ WaitForVBlank: .ENDS +.SECTION ".waitforthreevblanks_text" SUPERFREE + +; WaitForVBlankTiles3(void); +; +; Used by `setMosaicEffect`. +; +; KEEP: A, X, Y +; A unknown +; I unknown +; DB unknown +; D unknown +WaitForThreeVBlanks: + ; MUST NOT modify X, Y or A + jsl WaitForVBlank + jsl WaitForVBlank + jml WaitForVBlank + +.ENDS + + .SECTION ".waitnvblank_text" SUPERFREE ;--------------------------------------------------------------------------- diff --git a/pvsneslib/source/videos.asm b/pvsneslib/source/videos.asm index 36ebefd6..fd986395 100644 --- a/pvsneslib/source/videos.asm +++ b/pvsneslib/source/videos.asm @@ -125,7 +125,8 @@ setFadeEffect: ldx.b #$0 -: - wai + jsl WaitForVBlank + ; A,X,Y unchanged txa sta.l REG_INIDISP inx @@ -142,7 +143,8 @@ setFadeEffect: _fadeouteffect: ldx.b #$F -: - wai + jsl WaitForVBlank + ; A,X,Y unchanged txa sta.l REG_INIDISP dex @@ -176,7 +178,9 @@ setFadeEffectEx: ldx.b #$0 -: lda.b 10,s --- wai +-- + jsl WaitForVBlank + ; A,X,Y unchanged dea bne -- @@ -197,7 +201,9 @@ _sfeex1: ldx.b #$F -: lda.b 10,s --- wai +-- + jsl WaitForVBlank + ; A,X,Y unchanged dea bne -- @@ -231,9 +237,9 @@ setMosaicEffect: lda #$00 ldx.w #$0 -: - wai - wai - wai + jsl WaitForThreeVBlanks + ; A,X,Y unchanged + ora 8,s ; Enable effect for BG in parameters sta.l REG_MOSAIC clc @@ -254,9 +260,9 @@ _mosaicouteffect: lda #$F0 ldx.w #$0 --: wai - wai - wai +-: + jsl WaitForThreeVBlanks + ; A,X,Y unchanged ora 8,s ; Enable effect for BG in parameters sta.l REG_MOSAIC From d28e303f5813e6419e1045c274361c5d03fd9bc0 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Fri, 24 May 2024 19:48:10 +1000 Subject: [PATCH 088/106] Cleanup mouse read labels and document _MouseData inputs --- pvsneslib/source/vblank.asm | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index ef4d725d..7dc8c924 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -381,7 +381,7 @@ _GetScope: ;--------------------------------------------------------------------------------- -;* mouse_read +;* mouse read ;--------------------------------------------------------------------------------- @@ -415,7 +415,7 @@ _GetScope: ;; D = tcc__registers_irq (NOT ZERO) .ACCU 8 .INDEX 16 -_mouseRead: +_MouseRead: sep #$30 ; The code assumes Joypad Auto-Read is not active. @@ -423,7 +423,7 @@ _mouseRead: ldx #$01 lda REG_JOY2L ; Joy2 - jsr _mouse_data + jsr _MouseData lda connect_st+1 beq @_20 @@ -434,7 +434,7 @@ _mouseRead: @_20: dex lda REG_JOY1L ; Joy1 - jsr _mouse_data + jsr _MouseData lda connect_st beq @_30 @@ -454,9 +454,16 @@ _mouseRead: rtl +;; Read the mouse values from a single controller port. +;; +;; IN: A = REG_JOY1L or REG_JOY2L +;; IN: X = 0 or 1 +;; +;; DB = 0 +;; D = tcc__registers_irq (NOT ZERO) .accu 8 .index 8 -_mouse_data: +_MouseData: sta tcc__r0 ; (421A / 4218 saved to reg0) and.b #$0F @@ -639,7 +646,7 @@ FVBlank: lda snes_mouse beq + - jsl _mouseRead + jsl _MouseRead ; If both ports have a mouse plugged, it will skip pad controller reading lda.w mouseConnect From 71248fb7feea7e1ea1312000b765ead2252304be Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Fri, 24 May 2024 20:21:41 +1000 Subject: [PATCH 089/106] Fix _MouseRead speed_change call The speed_change jsr in `_MouseRead` can crash if `mouseSpeedChange` is in a different bank to `_MouseRead`. speed_change has been changed to a child label to illuminate that `_MouseRead` is calling a subroutine outside of vblank.asm. --- pvsneslib/source/input.asm | 13 ++++++++++--- pvsneslib/source/vblank.asm | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pvsneslib/source/input.asm b/pvsneslib/source/input.asm index b8d28f96..dc8602d3 100644 --- a/pvsneslib/source/input.asm +++ b/pvsneslib/source/input.asm @@ -355,7 +355,8 @@ detectSuperScope: ;--------------------------------------------------------------------------------- -.SECTION ".mousespeedchange_text" SUPERFREE +; Must be in bank 0, used by _MouseRead in the VBlank ISR. +.SECTION ".mousespeedchange_text" SEMIFREE BANK 0 ;--------------------------------------------------------------------------------- ; void mouseSpeedChange(u8 port) @@ -373,7 +374,7 @@ mouseSpeedChange: lda 8,s ; Set port tax - jsr speed_change + jsr @speed_change ply plx @@ -381,7 +382,13 @@ mouseSpeedChange: plp rtl -speed_change: + +; Called by _MouseRead in the Vblank ISR +; X = 0 or 1 +; DB = 0 +.ACCU 8 +.INDEX 8 +@speed_change: php sep #$30 diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 7dc8c924..210c2c70 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -428,7 +428,7 @@ _MouseRead: lda connect_st+1 beq @_20 - jsr speed_change + jsr mouseSpeedChange@speed_change stz connect_st+1 @_20: @@ -439,7 +439,7 @@ _MouseRead: lda connect_st beq @_30 - jsr speed_change + jsr mouseSpeedChange@speed_change stz connect_st @_30: From 5bfe11d491a648868d25d624423cdedb73db5654 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Fri, 24 May 2024 20:24:20 +1000 Subject: [PATCH 090/106] Remove mouse_read comments from input.asm mouse_read has been moved to vblank.asm and the same comments already exist in vblank.asm. --- pvsneslib/source/input.asm | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/pvsneslib/source/input.asm b/pvsneslib/source/input.asm index dc8602d3..e415a7d8 100644 --- a/pvsneslib/source/input.asm +++ b/pvsneslib/source/input.asm @@ -330,30 +330,6 @@ detectSuperScope: .ENDS -;--------------------------------------------------------------------------------- - -;* mouse_read - -;--------------------------------------------------------------------------------- - -;* If this routine is called every frame, then the mouse status will be set -;* to the appropriate registers. -;* INPUT -;* None (Mouse key read automatically) -;* OUTPUT -;* Connection status (mouse_con) D0=1 Mouse connected to Joyl -;* D1=1 Mouse connected to Joy2 -;* Switch (mousePressed,1) D0=left switch turbo -;* D1=right switch turbo -;* Switch (mouseButton,1) D0=left switch trigger -;* D1=right switch trigger -;* Mouse movement (ball) value -;* (mouse_x) D7=0 Positive turn, D7=1 Negative turn -;* D6-D0 X movement value -;* (mouse_y) D7=0 Positive turn, D7=1 Negative turn -;* D6-D0 X movement value - -;--------------------------------------------------------------------------------- ; Must be in bank 0, used by _MouseRead in the VBlank ISR. .SECTION ".mousespeedchange_text" SEMIFREE BANK 0 From d84abae6fe0532b065485a2d857038284a85c2c2 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Fri, 24 May 2024 20:29:01 +1000 Subject: [PATCH 091/106] Use jsr/rts to call _MouseRead Saves 2 cycles and matches the _GetScope call in the VBlank ISR. --- pvsneslib/source/vblank.asm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 210c2c70..45167ea3 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -451,7 +451,7 @@ _MouseRead: +: rep #$10 - rtl + rts ;; Read the mouse values from a single controller port. @@ -646,7 +646,7 @@ FVBlank: lda snes_mouse beq + - jsl _MouseRead + jsr _MouseRead ; If both ports have a mouse plugged, it will skip pad controller reading lda.w mouseConnect From c52383e5fa7f10bc04cee08a1aa7bfb832d27dc2 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Fri, 24 May 2024 17:29:24 +0200 Subject: [PATCH 092/106] fix(db): fix bank text vram offset for fps display --- pvsneslib/source/videos.asm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvsneslib/source/videos.asm b/pvsneslib/source/videos.asm index 0bf17034..7ff977bf 100644 --- a/pvsneslib/source/videos.asm +++ b/pvsneslib/source/videos.asm @@ -1198,7 +1198,7 @@ _sfctr1: rep #$20 and #$00FF clc - adc txt_vram_offset ; add text offset and put 16 bit value to VRAM + adc.l txt_vram_offset ; add text offset and put 16 bit value to VRAM sta.l $2118 sep #$20 @@ -1208,7 +1208,7 @@ _sfctr1: rep #$20 and #$00FF clc - adc txt_vram_offset + adc.l txt_vram_offset sta.l $2118 ; add text offset and put 16 bit value to VRAM plb From 2c5df2b8982dc9159075da0dbba095c693fed8c7 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sat, 25 May 2024 11:08:16 +1000 Subject: [PATCH 093/106] Fix potential nmiSet() crash Writes to nmi_handler are not atomic and the VBlank ISR can crash if a VBlank Interrupt occurs in the middle of the nmi_handler write. To fix this crash, nmiSet() is now a function that will temporarily disable interrupts so nmi_handler can be safely written to. This commit also modifies consoleInit() to use nmiSet(). --- pvsneslib/include/snes/interrupt.h | 33 ++++++++++++++++------ pvsneslib/source/consoles.asm | 19 ++++++++----- pvsneslib/source/crt0_snes.asm | 1 + pvsneslib/source/vblank.asm | 45 ++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 15 deletions(-) diff --git a/pvsneslib/include/snes/interrupt.h b/pvsneslib/include/snes/interrupt.h index 10e9295a..18090092 100644 --- a/pvsneslib/include/snes/interrupt.h +++ b/pvsneslib/include/snes/interrupt.h @@ -66,7 +66,17 @@ extern u8 vblank_flag; */ extern u16 lag_frame_counter; - +/** + * \brief VBlank routine + * + * This function is called on \b every VBlank interrupt by the VBlank Interrupt Service Routine. + * + * \b CAUTION: Writes to \c nmi_handler are not atomic and can cause a crash if a VBlank + * Interrupt occurs in the middle of the \c nmi_handler write. Use nmiSet() or disable NMI + * interrupts when modifying \c nmi_handler. + * + * \see nmiSet() + */ extern void *nmi_handler; /** \brief VBlank NMI Enable (0=Disable, 1=Enable) (Initially disabled on reset) */ @@ -171,14 +181,21 @@ The H/V-IRQ flag in Bit7 of TIMEUP, Port 4211h gets set when the V-Counter gets #define REG_HVBJOY (*(vuint8 *)0x4212) /** - * \brief - * Add a handler for the given interrupt mask.
+ * \brief Sets the #nmi_handler (VBlank routine). * - * Specify the handler to use for the nmi interrupt.
- * \param handler - * Address of the function to use as an interrupt service routine
-*/ -#define nmiSet(handler) nmi_handler = handler; + * This function will also disable any active IRQ interrupts, enable VBlank interrupts and enable Joypad Auto-Read. + * + * \param vblankRoutine the function to call on every VBlank (NMI) interrupt.
+ * + * \b CAUTION: \p vblankRoutine is called on \b every VBlank interrupt. + * #vblank_flag can be used to determine if \p vblankRoutine was called during a lag-frame. + * + * \b CAUTION: This function will override the default #nmi_handler. + * If you are using consoleDrawText(), you will need to call consoleVblank() inside \p vblankRoutine. + * + * \see #nmi_handler + */ +void nmiSet(void (*vblankRoutine)(void)); /** * \brief diff --git a/pvsneslib/source/consoles.asm b/pvsneslib/source/consoles.asm index c2b3cd67..fe66252e 100644 --- a/pvsneslib/source/consoles.asm +++ b/pvsneslib/source/consoles.asm @@ -418,11 +418,12 @@ consoleVblank: consoleInit: php + sep #$20 + + lda.b #0 ; Disable interrupts to prevent the VBlank ISR + sta.l REG_NMITIMEN ; from modifying VRAM, PPU Registers or variables. + rep #$20 - lda.w #:consoleVblank ; Put current handler to our function - sta.l nmi_handler + 2 - lda.w #consoleVblank - sta.l nmi_handler lda.w #0 ; Begin counting vblank sta.w snes_vblank_count @@ -496,10 +497,14 @@ consoleInit: lda #TXT_VRAMOFFSET sta txt_vram_offset - plb + ; Set nmi_handler, enable VBlank interrupts, enable joypad auto-read. + pea :consoleVblank + pea consoleVblank + jsl nmiSet + pla + pla - lda #INT_VBLENABLE | INT_JOYPAD_ENABLE ; enable NMI, enable autojoy - sta.l REG_NMITIMEN + plb plp rtl diff --git a/pvsneslib/source/crt0_snes.asm b/pvsneslib/source/crt0_snes.asm index e002e58b..77e0268e 100644 --- a/pvsneslib/source/crt0_snes.asm +++ b/pvsneslib/source/crt0_snes.asm @@ -245,6 +245,7 @@ fast_start: lda.w #tcc__registers tad + ; This nmi_handler write is safe. Interrupts are disabled by `tcc__snesinit` lda.w #EmptyNMI sta.b nmi_handler lda.w #:EmptyNMI diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 45167ea3..10cc12f3 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -693,10 +693,55 @@ __JumpTo_nmi_handler: .ENDS + ;--------------------------------------------------------------------------- +.BASE BASE_0 +.SECTION ".nmiSet_text" SUPERFREE + +; void nmiSet(void (*vblankRoutine)(void)); +; +; DB access lowram +nmiSet: + php +.DEFINE _stack_arg_offset 5 + + sep #$20 +.ACCU 8 + + ; Disable interrupts. + ; Prevents a crash if a VBlank Interrupt occurs in the middle of the two `nmi_handler` writes. + lda #0 + sta.l REG_NMITIMEN + + rep #$20 +.INDEX 16 + lda _stack_arg_offset,s + sta.w nmi_handler + + lda _stack_arg_offset + 2,s + sta.w nmi_handler + 2 + sep #$20 +.ACCU 8 + ; Reset the NMI flag. + ; Prevents an NMI interrupt from erroneously activating on the REG_NMITIMEN write. + lda.l REG_RDNMI + + ; Enable VBlank Interrupts and Joypad Auto-Read. + lda #INT_VBLENABLE | INT_JOYPAD_ENABLE + sta.l REG_NMITIMEN + +.UNDEFINE _stack_arg_offset + plp + rtl + +.ENDS + + +;--------------------------------------------------------------------------- + .BASE BASE_0 .SECTION ".waitforvblank_text" SUPERFREE From 4cb07a026830782906c3bb5ce0b97bd4220d8dff Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sat, 25 May 2024 11:15:19 +1000 Subject: [PATCH 094/106] Update snes-examples custom nmi_handlers * Test for lag-frames * Remove oamMemory DMA transfer (now handled by the VBlank ISR) * Remove snes_vblank_count++ (now handled by the VBlank ISR) --- .../Mode1ContinuosScroll.c | 26 ++++++------ .../Sprites/DynamicSprite/DynamicSprite.c | 40 +++++++++---------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c b/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c index a8d7ad54..2edf1270 100644 --- a/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c +++ b/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c @@ -184,23 +184,23 @@ void handleScrollSub(character *p, scroll *s) // interruption of vblank (send information to vram via DMA) void myconsoleVblank() { - - dmaCopyOAram((unsigned char *)&oamMemory, 0, 0x220); - - if (bg_mutex == 0) + if (vblank_flag) { - if (bgInfo.refreshBG1 == true) + if (bg_mutex == 0) { - dmaCopyVram(bgInfo.bg1.gfxoffset, bgInfo.bg1.adrgfxvram, bgInfo.bg1.size); - bgInfo.refreshBG1 = false; + if (bgInfo.refreshBG1 == true) + { + dmaCopyVram(bgInfo.bg1.gfxoffset, bgInfo.bg1.adrgfxvram, bgInfo.bg1.size); + bgInfo.refreshBG1 = false; + } } - } - if (bg_mutex == 0) - { - if (bgInfo.refreshBG2 == true) + if (bg_mutex == 0) { - dmaCopyVram(bgInfo.bg2.gfxoffset, bgInfo.bg2.adrgfxvram, bgInfo.bg2.size); - bgInfo.refreshBG2 = false; + if (bgInfo.refreshBG2 == true) + { + dmaCopyVram(bgInfo.bg2.gfxoffset, bgInfo.bg2.adrgfxvram, bgInfo.bg2.size); + bgInfo.refreshBG2 = false; + } } } } diff --git a/snes-examples/graphics/Sprites/DynamicSprite/DynamicSprite.c b/snes-examples/graphics/Sprites/DynamicSprite/DynamicSprite.c index e053e685..f19c09b6 100644 --- a/snes-examples/graphics/Sprites/DynamicSprite/DynamicSprite.c +++ b/snes-examples/graphics/Sprites/DynamicSprite/DynamicSprite.c @@ -27,31 +27,27 @@ spritequeue sprqueue[16]; // Max 16 entries in queue //--------------------------------------------------------------------------------- void myconsoleVblank(void) { - u8 *pgfx; - u16 padrgfx; + if (vblank_flag) { + u8 *pgfx; + u16 padrgfx; - // if tile sprite queued - if (spr_queue != 0xff) - { - if (spr_mutex == 0) - { // if we have finished adding things - // copy memory to vram (2 tiles of the 16x16 sprites) - while (spr_queue != 0xff) - { - pgfx = sprqueue[spr_queue].gfxoffset; - padrgfx = sprqueue[spr_queue].adrgfxvram; - dmaCopyVram(pgfx, padrgfx, 8 * 4 * 2); - dmaCopyVram(pgfx + 8 * 4 * 16, padrgfx + 8 * 4 * 8, 8 * 4 * 2); - spr_queue--; + // if tile sprite queued + if (spr_queue != 0xff) + { + if (spr_mutex == 0) + { // if we have finished adding things + // copy memory to vram (2 tiles of the 16x16 sprites) + while (spr_queue != 0xff) + { + pgfx = sprqueue[spr_queue].gfxoffset; + padrgfx = sprqueue[spr_queue].adrgfxvram; + dmaCopyVram(pgfx, padrgfx, 8 * 4 * 2); + dmaCopyVram(pgfx + 8 * 4 * 16, padrgfx + 8 * 4 * 8, 8 * 4 * 2); + spr_queue--; + } } } } - - // Put oam to screen if needed - dmaCopyOAram((unsigned char *)&oamMemory, 0, 0x220); - - // count vbls - snes_vblank_count++; } //--------------------------------------------------------------------------------- @@ -118,4 +114,4 @@ int main(void) WaitForVBlank(); } return 0; -} \ No newline at end of file +} From 0dcb7eeee876f3ae9c423b3029246dd4b8331896 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sat, 25 May 2024 19:57:21 +1000 Subject: [PATCH 095/106] Document the new VBlank ISR --- pvsneslib/include/snes/input.h | 13 +++++--- pvsneslib/include/snes/interrupt.h | 52 ++++++++++++++++++++++++------ pvsneslib/include/snes/sprite.h | 7 +++- pvsneslib/source/vblank.asm | 4 +++ 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/pvsneslib/include/snes/input.h b/pvsneslib/include/snes/input.h index 64f9a5d4..85789b19 100644 --- a/pvsneslib/include/snes/input.h +++ b/pvsneslib/include/snes/input.h @@ -27,9 +27,12 @@ ---------------------------------------------------------------------------------*/ -/*! \file input.h - \brief input support. -*/ +/*! + * \file input.h + * \brief input support. + * + * The inputs are automatically read by the \ref VBlank-ISR on non-lag frames. + */ #ifndef SNES_INPUT_INCLUDE #define SNES_INPUT_INCLUDE @@ -37,7 +40,7 @@ #include #include -/*! \file +/*! \brief common values for pad input. common values that can be used to test auto pad. @@ -59,7 +62,7 @@ typedef enum KEYPAD_BITS KEY_Y = BIT(14), //!< pad Y button. } KEYPAD_BITS; -/*! \file +/*! \brief common values for SuperScope input. */ //! enum values for the SuperScope buttons and flags. diff --git a/pvsneslib/include/snes/interrupt.h b/pvsneslib/include/snes/interrupt.h index 18090092..34d6d67e 100644 --- a/pvsneslib/include/snes/interrupt.h +++ b/pvsneslib/include/snes/interrupt.h @@ -27,9 +27,39 @@ ---------------------------------------------------------------------------------*/ -/*! \file interrupt.h - \brief snes interrupt support. -*/ +/*! + * \file interrupt.h + * \brief snes interrupt support. + * + *

VBlank ISR \anchor VBlank-ISR

+ * + * PVSnesLib includes a Vertical Blank Interrupt Service Routine (VBlank ISR or NMI ISR) that will + * do the following actions (in this order) at the start of the Vertical Blanking Period when + * VBlank interrupts are enabled: + * - Transfer #oamMemory to the PPU OAM on non lag-frames. + * - Call #nmi_handler. + * - Increment #snes_vblank_count. + * - Read inputs from the controller ports on non lag-frames (see input.h). + * - Increment #lag_frame_counter on lag-frames. + * + * A lag-frame is when the execution time between two WaitForVBlank() calls exceeds a 50/60Hz + * display frame. By testing for lag-frames, the VBlank ISR will not transfer a partially + * populated #oamMemory to the PPU OAM. + * + * Lag-frames are determined by the #vblank_flag variable, which is set on WaitForVBlank() and + * cleared in the VBlank ISR. + * + * Inputs are only read on non lag-frames to prevent the input state from unexpectedly changing in + * the middle of the main loop (potentially causing a heisenbug). Code that loops until a button + * is pressed must call WaitForVBlank() within the loop otherwise it will loop forever. + * + * The #nmi_handler function pointer (set using nmiSet()) is called on \b every VBlank interrupt + * (even during force-blank / setScreenOff()). + * To prevent glitches, #nmi_handler's should either: + * - Test #vblank_flag and only update the PPU registers/memory if the #vblank_flag is set. + * - Use locks or flags on every buffer/queue to prevent partially populated data from being transferred to the PPU. + * + */ #ifndef SNES_INTERRUPT_INCLUDE #define SNES_INTERRUPT_INCLUDE @@ -69,13 +99,16 @@ extern u16 lag_frame_counter; /** * \brief VBlank routine * - * This function is called on \b every VBlank interrupt by the VBlank Interrupt Service Routine. + * This function is called on \b every VBlank interrupt by the \ref VBlank-ISR "VBlank Interrupt Service Routine". * * \b CAUTION: Writes to \c nmi_handler are not atomic and can cause a crash if a VBlank * Interrupt occurs in the middle of the \c nmi_handler write. Use nmiSet() or disable NMI * interrupts when modifying \c nmi_handler. * - * \see nmiSet() + * Assembly note: This function pointer will be called with a non-zero Direct Page register + * to prevent \c nmi_handler from clobbering the tcc imaginary registers. + * + * \see \ref VBlank-ISR, nmiSet() */ extern void *nmi_handler; @@ -193,15 +226,16 @@ The H/V-IRQ flag in Bit7 of TIMEUP, Port 4211h gets set when the V-Counter gets * \b CAUTION: This function will override the default #nmi_handler. * If you are using consoleDrawText(), you will need to call consoleVblank() inside \p vblankRoutine. * - * \see #nmi_handler + * \see \ref VBlank-ISR, #nmi_handler */ void nmiSet(void (*vblankRoutine)(void)); /** - * \brief - * Wait for vblank interrupt
+ * \brief Waits for a VBlank interrupt. + * + * Sets the #vblank_flag and pauses execution until the #vblank_flag is cleared (by the \ref VBlank-ISR). * - * Waits for a vertical blank interrupt
+ * \b CAUTION: This function will loop forever if VBlank interrupts are disabled. * * Assembly note: This function will not modify the A/X/Y registers and can be called with an 8 or 16 bit .ACCU/.INDEX. */ diff --git a/pvsneslib/include/snes/sprite.h b/pvsneslib/include/snes/sprite.h index 1eb79160..fabdb600 100644 --- a/pvsneslib/include/snes/sprite.h +++ b/pvsneslib/include/snes/sprite.h @@ -109,7 +109,12 @@ Size bit (Small/Large) can be defined in OBSEL Register (Port 2101h).
*/ extern t_sprites oambuffer[128]; /*!< \brief current sprite buffer for dynamic engine */ -extern u8 oamMemory[128 * 4 + 8 * 4]; /*!< \brief to address oam table low and high */ +/** + * \brief to address oam table low and high + * + * This buffer is automatically transferred to the PPU OAM by the \ref VBlank-ISR on non lag-frames. + */ +extern u8 oamMemory[128 * 4 + 8 * 4]; /*! \def REG_OBSEL \brief Object Size and Object Base (W) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 10cc12f3..93e3bdaf 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -564,6 +564,10 @@ FVBlank: plb ; DB = $80 + + ; Whenever this ISR is modified, the `VBlank ISR` section in `interrupt.h` MUST ALSO be updated. + + ; Using 16 bit A so the `stz $2102` below clears a 16-bit register. sep #$10 .ACCU 16 From ff864f049064e319736ce49ec375d241d9bc6b7f Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sun, 26 May 2024 15:51:47 +1000 Subject: [PATCH 096/106] Fix padsDown(), padsUp() and padsClear() These functions did not multiply the argument by 2 when converting the `value` argument (0-4) to an X index register value (0, 2, 4, 6, 8). --- pvsneslib/source/input.asm | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pvsneslib/source/input.asm b/pvsneslib/source/input.asm index e415a7d8..2eaa4150 100644 --- a/pvsneslib/source/input.asm +++ b/pvsneslib/source/input.asm @@ -138,8 +138,8 @@ padsClear: rep #$20 lda 8,s ; get value - pha - plx + asl + tax sep #$20 lda #$0 @@ -170,8 +170,8 @@ padsDown: rep #$20 lda 8,s ; get value - pha - plx + asl + tax lda pad_keysold,x eor #$FFFF @@ -205,8 +205,8 @@ padsUp: rep #$20 lda 8,s ; get value - pha - plx + asl + tax lda pad_keys,x eor #$FFFF From 55a09246397180f08181e760df3e15d729aab629 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sun, 26 May 2024 16:19:36 +1000 Subject: [PATCH 097/106] Rename pad_keysrepeat to pad_keysdown and change padsDown() to a macro I have tested and confirmed `padsDown(i) == pad_keysrepeat[i]` before making this change. --- pvsneslib/include/snes/input.h | 11 +++++---- pvsneslib/source/input.asm | 42 ++++------------------------------ pvsneslib/source/vblank.asm | 8 +++---- 3 files changed, 14 insertions(+), 47 deletions(-) diff --git a/pvsneslib/include/snes/input.h b/pvsneslib/include/snes/input.h index 85789b19..bded5ffb 100644 --- a/pvsneslib/include/snes/input.h +++ b/pvsneslib/include/snes/input.h @@ -78,7 +78,7 @@ typedef enum SUPERSCOPE_BITS extern u16 pad_keys[5]; extern u16 pad_keysold[5]; -extern u16 pad_keysrepeat[5]; +extern u16 pad_keysdown[5]; extern u8 snes_mplay5; /*! \brief 1 if MultiPlay5 is connected */ extern u8 snes_mouse; /*! \brief 1 if Mouse is going to be used */ @@ -158,12 +158,13 @@ extern u16 scope_sinceshot; /*! \brief Number of frames elapsed since last shot // unsigned short padsCurrent(u16 value); #define padsCurrent(value) (pad_keys[value]) -/*! \fn padsDown(u16 value) +/*! \fn padsDown(value) \brief Return value of down keys for selected pad - \param value Address of the pad to use (0 or 1 to 4 if multiplayer 5 connected) - \return unsigned short of the current pad value + \param value Address of the pad to use (0-1 or 0-4 if multiplayer 5 connected) + \return unsigned short of the newly pressed down keys (0 -> 1 transition) */ -unsigned short padsDown(u16 value); +// unsigned short padsDown(u16 value); +#define padsDown(value) (pad_keysdown[value]) /*! \fn padsUp(u16 value) \brief Return value of up keys for selected pad diff --git a/pvsneslib/source/input.asm b/pvsneslib/source/input.asm index 2eaa4150..cbe25652 100644 --- a/pvsneslib/source/input.asm +++ b/pvsneslib/source/input.asm @@ -37,9 +37,9 @@ .BASE $00 .RAMSECTION ".reg_pads" BANK 0 SLOT 1 -pad_keys dsb 10 ; 5 pads , 16 bits reg -pad_keysold dsb 10 ; 5 pads , 16 bits reg -pad_keysrepeat dsb 10 ; 5 pads , 16 bits reg +pad_keys dsb 10 ; 5 pads , 16 bits reg +pad_keysold dsb 10 ; 5 pads , 16 bits reg +pad_keysdown dsb 10 ; 5 pads , 16 bits reg snes_mplay5 db ; 1 if MultiPlayer5 is connected mp5read db ; for multiplayer5 plug test @@ -145,7 +145,7 @@ padsClear: lda #$0 sta pad_keys,x sta pad_keysold,x - sta pad_keysrepeat,x + sta pad_keysdown,x plx plb @@ -153,40 +153,6 @@ padsClear: rtl .ENDS -.SECTION ".pads2_text" SUPERFREE - -;--------------------------------------------------------------------------------- -; unsigned short padsDown(unsigned short value) -; return (pad_keys[value] & ~pad_keysold[value]); -padsDown: - php - phb - phx - - sep #$20 ; change bank address to 0 - lda.b #$0 - pha - plb - - rep #$20 - lda 8,s ; get value - asl - tax - - lda pad_keysold,x - eor #$FFFF - sta.w tcc__r0 - - lda pad_keys,x - and.w tcc__r0 - sta.w tcc__r0 - - plx - plb - plp - rtl - -.ENDS .SECTION ".pads3_text" SUPERFREE diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index 93e3bdaf..ff832061 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -77,7 +77,7 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose sta pad_keys ; store 'current' state eor pad_keysold ; compute 'down' state from bits that and pad_keys ; have changed from 0 to 1 - sta pad_keysrepeat ; + sta pad_keysdown ; lda REG_JOY2L ; read joypad register #2 bit #$0F ; catch non-joypad input @@ -87,7 +87,7 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose sta pad_keys+2 ; store 'current' state eor pad_keysold+2 ; compute 'down' state from bits that and pad_keys+2 ; have changed from 0 to 1 - sta pad_keysrepeat+2 ; + sta pad_keysdown+2 ; sep #$20 .ENDM @@ -182,7 +182,7 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose sta.w __pad_n(pad_keys, 2) eor.w __pad_n(pad_keysold, 2) and.w __pad_n(pad_keys, 2) - sta.w __pad_n(pad_keysrepeat, 2) + sta.w __pad_n(pad_keysdown, 2) ; Process pads 3 & 4 .REPEAT 2 INDEX _I @@ -198,7 +198,7 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose + eor.w __pad_n(pad_keysold, @p) and.w __pad_n(pad_keys, @p) - sta.w __pad_n(pad_keysrepeat, @p) + sta.w __pad_n(pad_keysdown, @p) .ENDR From 4cd71fa1b9854b0d98ce3134450522646362a8ac Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sun, 26 May 2024 16:46:07 +1000 Subject: [PATCH 098/106] Fix and optimise padsClear() padsClear() is not clearing the high byte of the pad variables. Using long addressing is faster then changing DB register. Also: * Added bounds testing to padsClear() * padsClear() can be called with an 8 bit Index --- pvsneslib/source/input.asm | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/pvsneslib/source/input.asm b/pvsneslib/source/input.asm index cbe25652..0de89388 100644 --- a/pvsneslib/source/input.asm +++ b/pvsneslib/source/input.asm @@ -128,27 +128,22 @@ connect_st dsb 2 ; void padsClear(unsigned short value) padsClear: php - phb + rep #$30 phx - sep #$20 ; change bank address to 0 - lda.b #$0 - pha - plb - - rep #$20 - lda 8,s ; get value - asl - tax + lda 7,s ; value argument + cmp #5 + bcs + + asl + tax - sep #$20 - lda #$0 - sta pad_keys,x - sta pad_keysold,x - sta pad_keysdown,x + lda #$0 + sta.l pad_keys,x + sta.l pad_keysold,x + sta.l pad_keysdown,x + + plx - plb plp rtl .ENDS From 43c717cf5aa24f65efdd57a47e3312a9a5f8db2e Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sun, 26 May 2024 17:10:22 +1000 Subject: [PATCH 099/106] Optimise padsUp() --- pvsneslib/source/input.asm | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/pvsneslib/source/input.asm b/pvsneslib/source/input.asm index 0de89388..2a1737e2 100644 --- a/pvsneslib/source/input.asm +++ b/pvsneslib/source/input.asm @@ -152,37 +152,25 @@ padsClear: .SECTION ".pads3_text" SUPERFREE ;--------------------------------------------------------------------------------- -;unsigned short padsUp(unsigned short value) { -; return (pad_keys[value] ^ pad_keysold[value]) & (~pad_keys[value]); +;unsigned short padsUp(u16 value) padsUp: php - phb + rep #$30 phx - sep #$20 ; change bank address to 0 - lda.b #$0 - pha - plb - - rep #$20 - lda 8,s ; get value - asl - tax - - lda pad_keys,x - eor #$FFFF - sta.w tcc__r0 + lda 7,s ; value argument + asl + tax - lda pad_keys,x - eor.w pad_keysold,x - and.w tcc__r0 - sta.w tcc__r0 + ; return pad_keysold[value] & (~pad_keys[value]); + lda.l pad_keys,x + eor.w #0xFFFF + and.l pad_keysold,x + sta.b tcc__r0 - plx - plb + plx plp rtl - .ENDS From 74d583abe1f27d892e1b5f53292e6cf17a7917bc Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Sun, 26 May 2024 17:18:13 +1000 Subject: [PATCH 100/106] Improve input.h documentation * Document pad arrays. * pads*() value parameter is an index, not an address. * Clarify pads*() parameter range --- pvsneslib/include/snes/input.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pvsneslib/include/snes/input.h b/pvsneslib/include/snes/input.h index bded5ffb..0c897b93 100644 --- a/pvsneslib/include/snes/input.h +++ b/pvsneslib/include/snes/input.h @@ -76,9 +76,9 @@ typedef enum SUPERSCOPE_BITS SSC_NOISE = BIT(8), //!< superscope NOISE flag. } SUPERSCOPE_BITS; -extern u16 pad_keys[5]; -extern u16 pad_keysold[5]; -extern u16 pad_keysdown[5]; +extern u16 pad_keys[5]; //!< current pad value +extern u16 pad_keysold[5]; //!< previous pad value +extern u16 pad_keysdown[5]; //!< newly pressed down pad keys extern u8 snes_mplay5; /*! \brief 1 if MultiPlay5 is connected */ extern u8 snes_mouse; /*! \brief 1 if Mouse is going to be used */ @@ -152,7 +152,7 @@ extern u16 scope_sinceshot; /*! \brief Number of frames elapsed since last shot /*! \fn padsCurrent(value) \brief Return current value of selected pad - \param value Address of the pad to use (0 or 1 to 4 if multiplayer 5 connected) + \param value pad index to use (0-1 or 0-4 if multiplayer 5 connected) \return unsigned short of the current pad value */ // unsigned short padsCurrent(u16 value); @@ -160,7 +160,7 @@ extern u16 scope_sinceshot; /*! \brief Number of frames elapsed since last shot /*! \fn padsDown(value) \brief Return value of down keys for selected pad - \param value Address of the pad to use (0-1 or 0-4 if multiplayer 5 connected) + \param value pad index to use (0-1 or 0-4 if multiplayer 5 connected) \return unsigned short of the newly pressed down keys (0 -> 1 transition) */ // unsigned short padsDown(u16 value); @@ -168,14 +168,14 @@ extern u16 scope_sinceshot; /*! \brief Number of frames elapsed since last shot /*! \fn padsUp(u16 value) \brief Return value of up keys for selected pad - \param value Address of the pad to use (0 or 1 to 4 if multiplayer 5 connected) - \return unsigned short of the current pad value + \param value pad index to use (0-1 or 0-4 if multiplayer 5 connected) + \return unsigned short of the released keys (1 -> 0 transition) */ unsigned short padsUp(u16 value); /*! \fn padsClear(u16 value) \brief Clear internal variables for selected pad - \param value Address of the pad to use (0 or 1 to 4 if multiplayer 5 connected) + \param value pad index to use (0-1 or 0-4 if multiplayer 5 connected) */ void padsClear(u16 value); From 537aa5ae796ac1c33d7275b9329c44f28dd56a8a Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Mon, 27 May 2024 16:56:49 +1000 Subject: [PATCH 101/106] Rename tcc__registers_irq to tcc__registers_nmi_isr It is only used in the NMI ISR and should not be used for IRQ interrupts. --- pvsneslib/source/crt0_snes.asm | 9 ++++++--- pvsneslib/source/vblank.asm | 16 ++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pvsneslib/source/crt0_snes.asm b/pvsneslib/source/crt0_snes.asm index 77e0268e..80549a01 100644 --- a/pvsneslib/source/crt0_snes.asm +++ b/pvsneslib/source/crt0_snes.asm @@ -25,9 +25,12 @@ tcc__f3 dsb 2 tcc__f3h dsb 2 move_insn dsb 4 ; 3 bytes mvn + 1 byte rts move_backwards_insn dsb 4 ; 3 bytes mvp + 1 byte rts - -tcc__registers_irq dsb 0 -tcc__regs_irq dsb 48 + +; Imaginary registers for the NMI ISR. +; Used to prevent the VBlank interrupts from clobbering the tcc imaginary registers (`tcc__registers`). +; MUST NOT be used for IRQ interrupts unless you know for certain IRQ and NMI interrupts will not overlap. +; MUST be >= sizeof(tcc imaginary registers). +tcc__registers_nmi_isr dsb 48 .ENDS diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index ff832061..cd5ac1d8 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -57,7 +57,7 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose ;; ACCU 8 ;; INDEX 16 ;; DB = 0 -;; D = tcc__registers_irq (NOT ZERO) +;; D = tcc__registers_nmi_isr (NOT ZERO) .MACRO _ScanPads ldy pad_keys ; copy joy states #1&2 sty pad_keysold @@ -109,7 +109,7 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose ;; ACCU 8 ;; INDEX 16 ;; DB = 0 -;; D = tcc__registers_irq (NOT ZERO) +;; D = tcc__registers_nmi_isr (NOT ZERO) .MACRO _ScanMPlay5 ; Using the multitap reading protocol from the SNES Development Wiki ; https://snes.nesdev.org/wiki/Multitap @@ -248,7 +248,7 @@ snes_frame_count_svg dsb 2 ; same thing for saving purpose ;; (This subroutine DOES NOT contain a REG_HVBJOY test/spinloop.) ;; ;; DB = 0 -;; D = tcc__registers_irq (NOT ZERO) +;; D = tcc__registers_nmi_isr (NOT ZERO) .ACCU 8 .INDEX 16 _GetScope: @@ -412,7 +412,7 @@ _GetScope: ;; (This subroutine DOES NOT contain a REG_HVBJOY test/spinloop.) ;; ;; DB = 0 -;; D = tcc__registers_irq (NOT ZERO) +;; D = tcc__registers_nmi_isr (NOT ZERO) .ACCU 8 .INDEX 16 _MouseRead: @@ -460,7 +460,7 @@ _MouseRead: ;; IN: X = 0 or 1 ;; ;; DB = 0 -;; D = tcc__registers_irq (NOT ZERO) +;; D = tcc__registers_nmi_isr (NOT ZERO) .accu 8 .index 8 _MouseData: @@ -556,9 +556,9 @@ FVBlank: ; Using a different Direct Page Register value to prevent the `nmi_handler` call ; from clobbering tcc imaginary registers. - lda.w #tcc__registers_irq + lda.w #tcc__registers_nmi_isr tad -; D = tcc__registers_irq +; D = tcc__registers_nmi_isr pea $7e80 plb @@ -690,7 +690,7 @@ FVBlank: ;; ACCU 16 ;; INDEX 16 ;; DB = $7e -;; D = tcc__registers_irq +;; D = tcc__registers_nmi_isr __JumpTo_nmi_handler: ; The `JML [addr]` instruction will always read the new program counter from Bank 0 jml [nmi_handler] From 4b3ec6d14e0390fb25157f0edd3bdd569feb271b Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Mon, 27 May 2024 17:03:42 +1000 Subject: [PATCH 102/106] Force tcc imaginary registers to address 0 Fixes potential memory corruption in NMI ISR. --- pvsneslib/source/crt0_snes.asm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pvsneslib/source/crt0_snes.asm b/pvsneslib/source/crt0_snes.asm index 80549a01..a0d4193d 100644 --- a/pvsneslib/source/crt0_snes.asm +++ b/pvsneslib/source/crt0_snes.asm @@ -1,6 +1,8 @@ .include "hdr.asm" -.RAMSECTION ".registers" BANK 0 SLOT 1 PRIORITY 1 +; tcc imaginary registers must start at address $00:0000 to ensure the NMI ISR writes to +; the correct addresses when the Direct Page Register is `tcc__registers_nmi_isr`. +.RAMSECTION ".registers" BANK 0 SLOT 1 ORGA 0 FORCE PRIORITY 1000 tcc__registers dsb 0 tcc__r0 dsb 2 tcc__r0h dsb 2 From f77e5c102a73431e56584aabdcea0740bebea0dd Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Mon, 27 May 2024 17:51:19 +1000 Subject: [PATCH 103/106] Optimise VBlank ISR by page aligning tcc__registers_nmi_isr The VBlank ISR changes the Direct Page Register to `tcc__registers_nmi_isr`. If `tcc__registers_nmi_isr` is not page aligned, a 1 cycle penalty will be applied to all direct-page instructions in the VBlank ISR (including the `nmi_handler`). --- pvsneslib/source/crt0_snes.asm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pvsneslib/source/crt0_snes.asm b/pvsneslib/source/crt0_snes.asm index a0d4193d..3d4f9dc1 100644 --- a/pvsneslib/source/crt0_snes.asm +++ b/pvsneslib/source/crt0_snes.asm @@ -27,6 +27,11 @@ tcc__f3 dsb 2 tcc__f3h dsb 2 move_insn dsb 4 ; 3 bytes mvn + 1 byte rts move_backwards_insn dsb 4 ; 3 bytes mvp + 1 byte rts +.ENDS + + +; `tcc__registers_nmi_isr` should be page-aligned to prevent a `D.l != 0` direct-page cycle penalty. +.RAMSECTION ".vblank_imagingary_registers" BANK 0 SLOT 1 ALIGN 0x100 ; Imaginary registers for the NMI ISR. ; Used to prevent the VBlank interrupts from clobbering the tcc imaginary registers (`tcc__registers`). @@ -36,6 +41,7 @@ tcc__registers_nmi_isr dsb 48 .ENDS + ; sections "globram.data" and "glob.data" can stay here in the file ; because we are using wla-65816 -d switch to disable WLA's ability to calculate A-B where A and B are labels. ; If you remove the -d switch, move those two sections to the very end of the source file, then WLA cannot calculate SECTIONEND_glob.data-SECTIONSTART_glob.data and it should be delayed for WLALINK to calculate From f13604589a52bab729f12eb9be80013a553bfe3f Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Tue, 28 May 2024 21:12:48 +1000 Subject: [PATCH 104/106] Remove bg_mutex from Mode1ContinuosScroll example `vblank_flag` can be used to determine if it is OK to read from `bgInfo` in `myconsoleVblank()`. --- .../Mode1ContinuosScroll.c | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c b/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c index 2edf1270..70a69a2a 100644 --- a/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c +++ b/snes-examples/graphics/Backgrounds/Mode1ContinuosScroll/Mode1ContinuosScroll.c @@ -76,32 +76,29 @@ character player1; // keep the joypad commands unsigned short pad0; -// handle a mutex to avoid refresh background while we are changing it -unsigned char bg_mutex; - // control where the background needs to be updated background bgInfo; // set background 1 to be updated void updateBG1(u8 *pgfx, u16 adrspr, int size) { - bg_mutex = 1; // to avoid vbl during queue management + // Safe to write to `bgInfo` - `myconsoleVBlank` will not read from `bgInfo` on lag-frames. + bgInfo.bg1.adrgfxvram = adrspr; bgInfo.bg1.gfxoffset = pgfx; bgInfo.bg1.size = size; bgInfo.refreshBG1 = true; - bg_mutex = 0; // to avoid vbl during queue management } // set background 2 to be updated void updateBG2(u8 *pgfx, u16 adrspr, int size) { - bg_mutex = 1; // to avoid vbl during queue management + // Safe to write to `bgInfo` - `myconsoleVBlank` will not read from `bgInfo` on lag-frames. + bgInfo.bg2.adrgfxvram = adrspr; bgInfo.bg2.gfxoffset = pgfx; bgInfo.bg2.size = size; bgInfo.refreshBG2 = true; - bg_mutex = 0; // to avoid vbl during queue management } void updatePos(character *p, unsigned short pad) @@ -184,23 +181,21 @@ void handleScrollSub(character *p, scroll *s) // interruption of vblank (send information to vram via DMA) void myconsoleVblank() { + // `vblank_flag` is set if the main-loop has finished the frame (not a lag-frame). + // `vblank_flag` is also used to confirm it is safe to read from `bgInfo`. + // (must not write to `vblank_flag` in this function). if (vblank_flag) { - if (bg_mutex == 0) + if (bgInfo.refreshBG1 == true) { - if (bgInfo.refreshBG1 == true) - { - dmaCopyVram(bgInfo.bg1.gfxoffset, bgInfo.bg1.adrgfxvram, bgInfo.bg1.size); - bgInfo.refreshBG1 = false; - } + dmaCopyVram(bgInfo.bg1.gfxoffset, bgInfo.bg1.adrgfxvram, bgInfo.bg1.size); + bgInfo.refreshBG1 = false; } - if (bg_mutex == 0) + + if (bgInfo.refreshBG2 == true) { - if (bgInfo.refreshBG2 == true) - { - dmaCopyVram(bgInfo.bg2.gfxoffset, bgInfo.bg2.adrgfxvram, bgInfo.bg2.size); - bgInfo.refreshBG2 = false; - } + dmaCopyVram(bgInfo.bg2.gfxoffset, bgInfo.bg2.adrgfxvram, bgInfo.bg2.size); + bgInfo.refreshBG2 = false; } } } @@ -226,7 +221,6 @@ int main(void) bgInitTileSet(2, &BG3_tiles, &BG3_pal, 0, (&BG3_tiles_end - &BG3_tiles), 16 * 4, BG_4COLORS, 0x4000); // queue BG1 and BG2 to be updated in the vblank interruption - bg_mutex = 0; updateBG1(&BG1_map, 0x0000, 2048); updateBG2(&BG2_map, 0x0000 + 2048, 2048); From 1333a145512582a6c8fe81be021db6b6657b8c31 Mon Sep 17 00:00:00 2001 From: Marcus Rowe Date: Tue, 28 May 2024 21:57:01 +1000 Subject: [PATCH 105/106] Move getFPScounter() variables to videos.asm --- pvsneslib/source/vblank.asm | 5 +---- pvsneslib/source/videos.asm | 11 +++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pvsneslib/source/vblank.asm b/pvsneslib/source/vblank.asm index cd5ac1d8..551ecf06 100644 --- a/pvsneslib/source/vblank.asm +++ b/pvsneslib/source/vblank.asm @@ -33,10 +33,7 @@ nmi_handler dsb 4 lag_frame_counter dsb 2 ; Number of lag frames encountered (can be externally modified) -snes_vblank_count dsb 2 ; 2 bytes to count number of vblank -snes_vblank_count_svg dsb 2 ; same thing for saving purpose -snes_frame_count dsb 2 ; 2 bytes for frame counter inside loop -snes_frame_count_svg dsb 2 ; same thing for saving purpose +snes_vblank_count dsb 2 ; Incremented every VBlank interrupt .ENDS diff --git a/pvsneslib/source/videos.asm b/pvsneslib/source/videos.asm index fd986395..d36b303c 100644 --- a/pvsneslib/source/videos.asm +++ b/pvsneslib/source/videos.asm @@ -101,6 +101,17 @@ m7_md DSB (225-64)*3 ; 483 bytes .ENDS + +; getFPScounter() variables +.RAMSECTION ".getfpscounter_lowram" BANK 0 SLOT 1 + +snes_vblank_count_svg dsb 2 ; for comparing snes_vblank_count +snes_frame_count dsb 2 ; 2 bytes for frame counter inside loop +snes_frame_count_svg dsb 2 ; same thing for saving purpose + +.ENDS + + .BASE BASE_0 .SECTION ".videos0_text" SUPERFREE From f49bae679ba58f091e8853a44152d523aa6856e4 Mon Sep 17 00:00:00 2001 From: Alekmaul Date: Mon, 10 Jun 2024 09:16:21 +0200 Subject: [PATCH 106/106] chore(*): change version to 4.3 and current day --- devkitsnes/readme.txt | 2 +- pvsneslib/pvsneslib_license.txt | 2 +- readme.md | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/devkitsnes/readme.txt b/devkitsnes/readme.txt index abae1cd2..6972d49c 100644 --- a/devkitsnes/readme.txt +++ b/devkitsnes/readme.txt @@ -30,7 +30,7 @@ SPECIAL THANKS CHANGE LOG ---------------------- -VERSION V4.3.0 (March,24,2024) +VERSION V4.3.0 (June,10,2024) - See github changelog of the release VERSION V4.2.1 (March,04,2024) diff --git a/pvsneslib/pvsneslib_license.txt b/pvsneslib/pvsneslib_license.txt index 2030ef8b..8788dd9c 100644 --- a/pvsneslib/pvsneslib_license.txt +++ b/pvsneslib/pvsneslib_license.txt @@ -21,7 +21,7 @@ -------------------------------------------------------------------------- CHANGE LOG -------------------------------------------------------------------------- -V4.3.0 (March,24,2024) +V4.3.0 (June,10,2024) - See github changelog of the release V4.2.1 (March,04,2024) diff --git a/readme.md b/readme.md index 5e80f248..6d1360da 100644 --- a/readme.md +++ b/readme.md @@ -23,9 +23,9 @@ Here are good entry points to know how Super Nintendo works: ![Downloads](https://img.shields.io/github/downloads/alekmaul/pvsneslib/total?label=Total%20Downloads) -PVSnesLib Windows Release -PVSnesLib Linux Release -PVSnesLib MacOS Release +PVSnesLib Windows Release +PVSnesLib Linux Release +PVSnesLib MacOS Release To install the library, please download the latest [release](https://github.com/alekmaul/pvsneslib/releases/latest) or with the link below and follow instructions on the [Wiki pages](https://github.com/alekmaul/pvsneslib/wiki). @@ -100,7 +100,7 @@ Sydney Hunter by [CollectorVision](https://collectorvision.com/store/shop/snes/s - [**Mills32**](https://github.com/mills32/) for his mode7 3D example. - [**N_Arno**](https://github.com/nArnoSNES/) for his help on Linux version. - [**DigiDwrf**](https://github.com/DigiDwrf/) for hirom / fastrom support and also mouse & superscope support. -- [**undisbeliever**](https://github.com/undisbeliever/castle_platformer/) for map engine example. +- [**undisbeliever**](https://github.com/undisbeliever/castle_platformer/) for the great update to vblank code and the map engine example. And, of course, all the [**discord community**](https://discord.gg/DzEFnhB) !