From ef10cd6f8612554fd15641f236569354c7ad02b5 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Sun, 30 Aug 2026 11:45:20 +0200 Subject: [PATCH 1/3] Implement some missing Hyppo DOS traps, with long filename support Several DOS trap slots were bare labels sharing a single jmp invalid_subfunction, so calling them just reported an invalid sub-function. This fills some of them in, finishes fstat, and extends mkfile. All of them follow the existing convention: sub-function in A, STA $D640, then CLV, and carry clear on return means failure with a reason in dos_geterrorcode (A=$38). A=$08 getdisksize Y = destination page. Fills $YY00 with the fields already tracked in dos_disk_table: sectors per cluster, cluster count, FAT size and so on. It does not count free clusters, which would mean a full FAT scan. A=$0A getcwd Y = destination page. $YY00 = drive number, $YY01 = offset of a NUL-terminated path within the same page. Resolved on demand by walking the cwd up to the root - there is no persistent path to go stale, so the answer is right however the cwd got where it is. A=$0E mkdir Name via setname (A=$2E) first. Creates a sub-directory in the cwd, with its own "." and ".." entries. Fails with file_exists if the name is taken. A=$10 rmdir Name via setname first. Refuses anything that is not a directory, or a directory holding more than "." and "..". A=$1E mkfile Already worked for 8.3 names, contiguous allocation and all. Now runs the name through the long-name path, reserving the extra dirent slots the VFAT pieces need rather than the single slot it used to take. A=$24 seekfile X/Y/Z = 24-bit sector number within the open file. Walks the cluster chain forward without transferring any data, which is the cheap way to reach a sector - reading there instead would move every byte in between. FAT32 chains are singly linked, so seeking is always from the start, and there is no length to bounds-check against: going past the end fails when the chain runs out. A=$28 fstat Existed, but never called hypervisor_setup_copy_region, so it wrote through whatever destination the previous trap had established - after the usual setname + findfile that is the caller's filename buffer - and ignored its own Y. Now Y = destination page for the raw 32-byte dirent. A=$2A rename Renames the open file, or the open directory from opendir. A name that still fits 8.3 is rewritten in place; one that needs VFAT pieces moves the entry to a large enough run of free slots. Either way the attributes, dates, cluster and length are carried across untouched. Long filenames -------------- Hyppo could already read long names - dos_readdir has understood VFAT pieces for a long time, behind the enable switch that syspart patches into disable_lfn_byte. What it could not do is create them, so anything Hyppo wrote got a plain 8.3 name regardless. That side is now implemented: pieces are written with their checksum, short names are built by truncating and appending ~1 through ~9 until one does not collide with an existing entry, the preceding pieces are removed when a file is deleted, and rename relocates an entry when the new name needs more slots than the old one occupied. getcwd returns the long name of each level where there is one. The existing switch still governs all of it. With long names disabled every path falls back to 8.3, and so do entries that have no long name or whose pieces fail their checksum against the short entry - a stale chain is ignored rather than trusted. One consequence worth knowing: a path segment can now be 64 characters instead of 12, so getcwd's 255-byte buffer can be exhausted at about four levels rather than nineteen. It checks before writing and returns dos_errorcode_path_too_long, an error code that has been defined and unused until now. Freeze and unfreeze ------------------- A task's current directory is now carried across a freeze in the process descriptor. Previously, a task frozen in one directory always resumed in the root directory. Now saving cwd on freeze and on resume the saved cluster is only trusted if the directory is still listed in its parent, since removing a directory leaves its own "." and ".." intact and a freed cluster may since have been handed to a file. If it has gone, its parent is tried in turn. Other changes / Hyppo space usage --------------------------------- The 4510's 16-bit relative branches replace the branch-over-jmp idiom in twenty places, buying back 43 bytes. Even so, this uses up most of what hyppo had left: the space below DOSDiskTable_Start goes from about 1.6KB free to 38 bytes. ACME is now run with --strict-segments, because the fixed data areas from $BB00 up are placed by winding * backwards, and it otherwise only warns once code has already grown into them. Are these changes worth the space it takes up? Do we want to use the space available to other traps instead? Tests ----- hyppotest could not execute the branches: of the ten opcodes only BCC was both present and correct, four took their displacement from the low byte alone and four were missing entirely. --- .gitignore | 5 + Makefile | 7 +- src/hyppo/audiomix.asm | 10 +- src/hyppo/constants.asm | 2 + src/hyppo/dos.asm | 4042 +++++++++++++++++++++--------- src/hyppo/dos_write.asm | 3 +- src/hyppo/freeze.asm | 176 +- src/hyppo/freeze.test | 43 + src/hyppo/main.asm | 302 ++- src/hyppo/mem.asm | 2 +- src/hyppo/process_descriptor.asm | 15 +- src/hyppo/sdfat.asm | 46 +- src/hyppo/syspart.asm | 43 +- src/hyppo/targetsetup.asm | 4 +- src/hyppo/task.asm | 10 +- src/tools/hyppotest.c | 54 +- 16 files changed, 3224 insertions(+), 1540 deletions(-) create mode 100644 src/hyppo/freeze.test diff --git a/.gitignore b/.gitignore index 1ebc23d36..622fd75a9 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ src/tests/*.list src/tests/*.map src/tests/*.label src/tests/*.lbl +src/tests/*.sym src/tools/*.list src/tools/*.map src/tools/*.label @@ -145,3 +146,7 @@ PASS.* # jenkins build artifacts JENKINS_BUILD_ID JENKINS_BUILD_VERSION + +# local hyppo DOS-trap exercisers, not part of the build +src/tests/hyppo_dos_traps_test.a65 +src/tests/hyppo_dos_util.a65 diff --git a/Makefile b/Makefile index 658fe0bd4..147e06173 100644 --- a/Makefile +++ b/Makefile @@ -1089,11 +1089,14 @@ $(BINDIR)/border.prg: $(SRCDIR)/border.a65 $(OPHIS_DEPEND) $(OPHIS) $(OPHISOPT) $< -l $(BINDIR)/border.list -m $*.map -o $(BINDIR)/border.prg # ============================ done moved, print-warn, clean-target +# --strict-segments: the fixed data areas at $BB00 and up wind * backwards, +# so code growing past them would otherwise only warn and quietly overwrite +# the tail of hyppo. $(BINDIR)/HICKUP.M65: $(ACME_DEPEND) $(wildcard $(SRCDIR)/hyppo/*.asm) $(SRCDIR)/version.asm - $(ACME) --cpu m65 --setpc 0x8000 -l src/hyppo/HICKUP.sym -r src/hyppo/HICKUP.rep -I $(SRCDIR)/hyppo -DDEBUG_HYPPO=$(DEBUG_HYPPO) $(SRCDIR)/hyppo/main.asm + $(ACME) --cpu m65 --strict-segments --setpc 0x8000 -l src/hyppo/HICKUP.sym -r src/hyppo/HICKUP.rep -I $(SRCDIR)/hyppo -DDEBUG_HYPPO=$(DEBUG_HYPPO) $(SRCDIR)/hyppo/main.asm $(BINDIR)/BRICKUP.M65: $(ACME_DEPEND) $(wildcard $(SRCDIR)/hyppo/*.asm) $(SRCDIR)/version.asm - $(ACME) --cpu m65 --setpc 0x8000 -l src/hyppo/HICKUP.sym -r src/hyppo/HICKUP.rep -I $(SRCDIR)/hyppo -DDEBUG_HYPPO=$(DEBUG_HYPPO) $(SRCDIR)/hyppo/joyflash.asm + $(ACME) --cpu m65 --strict-segments --setpc 0x8000 -l src/hyppo/HICKUP.sym -r src/hyppo/HICKUP.rep -I $(SRCDIR)/hyppo -DDEBUG_HYPPO=$(DEBUG_HYPPO) $(SRCDIR)/hyppo/joyflash.asm $(UTILDIR)/cpusim.prg: $(ACME_DEPEND) $(UTILDIR)/cpusim.asm $(ACME) --cpu m65 --setpc 0x8100 -l cpusim.sym -r cpusim.rep $(UTILDIR)/cpusim.asm diff --git a/src/hyppo/audiomix.asm b/src/hyppo/audiomix.asm index c4f88013b..10eafbd71 100644 --- a/src/hyppo/audiomix.asm +++ b/src/hyppo/audiomix.asm @@ -79,7 +79,7 @@ audiomix_set_sid_lr_coefficients: ldx #$22 jsr audiomix_set2coefficients ldx #$32 - jmp audiomix_set2coefficients + bra audiomix_set2coefficients audiomix_set_sid_rl_coefficients: ldx #$c2 @@ -97,7 +97,7 @@ audiomix_set_sid_rl_coefficients: ldx #$20 jsr audiomix_set2coefficients ldx #$30 - jmp audiomix_set2coefficients + bra audiomix_set2coefficients audio_set_stereo: ;; Left and right SID volume levels @@ -105,7 +105,7 @@ audio_set_stereo: lda #$be jsr audiomix_set_sid_lr_coefficients lda #$40 - jmp audiomix_set_sid_rl_coefficients + bra audiomix_set_sid_rl_coefficients audio_set_stereomirrored: ;; Left and right SID volume levels @@ -113,7 +113,7 @@ audio_set_stereomirrored: lda #$40 jsr audiomix_set_sid_lr_coefficients lda #$be - jmp audiomix_set_sid_rl_coefficients + bra audiomix_set_sid_rl_coefficients audiomix_setcoefficient: stx audiomix_addr @@ -144,4 +144,4 @@ audiomix_set4coefficients: audiomix_set2coefficients: jsr audiomix_setcoefficient inx - jmp audiomix_setcoefficient + bra audiomix_setcoefficient diff --git a/src/hyppo/constants.asm b/src/hyppo/constants.asm index f0338e0a1..7d9ceb495 100644 --- a/src/hyppo/constants.asm +++ b/src/hyppo/constants.asm @@ -39,6 +39,8 @@ dos_errorcode_file_exists = $8D dos_errorcode_directory_full = $8E dos_errorcode_double_attach = $8F + dos_errorcode_directory_not_empty = $90 + dos_errorcode_path_too_long = $91 dos_errorcode_eof = $FF ;; FAT directory entry constants diff --git a/src/hyppo/dos.asm b/src/hyppo/dos.asm index 582b6b4fd..6089fa275 100644 --- a/src/hyppo/dos.asm +++ b/src/hyppo/dos.asm @@ -3,8 +3,6 @@ ;; Paul Gardner-Stephen, 2014-2019. ;; ---------------------------------------------------------------- */ - ;; XXX - Track down why 2nd and subsequent LFN blocks are not used. - dos_and_process_trap: ;; XXX - Machine is being updated to automatically disable IRQs on trapping @@ -34,32 +32,32 @@ dos_and_process_trap_table: ;; !16 trap_dos_getversion !16 trap_dos_getdefaultdrive - !16 trap_dos_getcurrentdrive ;; appears out-of-order (is far below) + !16 trap_dos_getcurrentdrive !16 trap_dos_selectdrive - !16 trap_dos_getdisksize ;; not currently implemented - !16 trap_dos_getcwd ;; not currently implemented + !16 trap_dos_getdisksize + !16 trap_dos_getcwd !16 trap_dos_chdir - !16 trap_dos_mkdir ;; not currently implemented + !16 trap_dos_mkdir ;; $10 - $1E ;; - !16 trap_dos_rmdir ;; not currently implemented + !16 trap_dos_rmdir !16 trap_dos_opendir !16 trap_dos_readdir !16 trap_dos_closedir !16 trap_dos_openfile !16 trap_dos_readfile !16 trap_dos_writefile - !16 trap_dos_mkfile ;; implementation started + !16 trap_dos_mkfile ;; $20 - $2E ;; !16 trap_dos_closefile !16 trap_dos_closeall - !16 trap_dos_seekfile ;; not currently implemented + !16 trap_dos_seekfile !16 trap_dos_rmfile - !16 trap_dos_fstat ;; implementation started - !16 trap_dos_rename ;; not currently implemented + !16 trap_dos_fstat + !16 trap_dos_rename !16 trap_dos_filedate ;; not currently implemented !16 trap_dos_setname @@ -122,48 +120,11 @@ trap_serial_monitor_write: sty hypervisor_write_char_to_serial_monitor jmp return_from_trap_with_success -;; ======================== - -trap_task_get_mapping: - jsr hypervisor_setup_copy_region - bcc @bad - ldy #5 -@copyloop: - lda hypervisor_maplohi,y - sta (" + bra return_from_trap_with_carry_flag +gds_havearea: - jmp return_from_trap_with_carry_flag + ldx dos_disk_table_offset + ldy #0 -trap_dos_setname: + lda dos_disk_table + fs_sector_count + 0,x + sta (" + sec + bra return_from_trap_with_carry_flag +;; ---- trap_dos_getcwd ---- + +trap_dos_getcwd: + ;; Returns the current working directory as a path string + ;; (e.g. "/FOO/BAR"), resolved on demand by walking cwd up to + ;; root (no persistent path tracking - always correct + ;; regardless of how cwd got there). Each level's name is found + ;; by reading its own ".." entry for the parent cluster, then + ;; scanning the parent for the entry whose cluster matches. + ;; Assumes a well-formed filesystem - a cyclic ".." chain would + ;; loop forever, same trust level as the rest of Hyppo's FAT32 + ;; handling. + ;; + ;; A level contributes its long name where it has one and long + ;; names are enabled; otherwise its 8.3 short name. That makes a + ;; segment up to 64 characters rather than 12, so the path can + ;; outgrow the 255-byte area at about four levels of long names, + ;; and the call then fails with dos_errorcode_path_too_long + ;; rather than running back over the two bytes below. + ;; + ;; Y: MSB of destination area. + ;; Output, starting at $YY00: + ;; $00 byte current disk number + ;; $01 byte offset (within this same $YY00 area) of the + ;; null-terminated path string, e.g. "/FOO/BAR" + ;; + ;; Example: + ;; LDY #$80 ; destination page for the result + ;; LDA #$0A : STA $D640 : CLV : BCC error jsr hypervisor_setup_copy_region - bcc tdsnfailure + bcs gcwd_havearea + +Checkpoint "trap_dos_getcwd " + bra return_from_trap_with_carry_flag +gcwd_havearea: - ldx " jmp return_from_trap_with_failure -;; ======================== +gcwd_checkentry: + ;; Found it - prepend "/" and its name to the path buffer, + ;; growing backward from the cursor. dos_readdir has already put + ;; the long name in dos_dirent_longfilename if the entry has one + ;; and long names are enabled; otherwise it leaves the length at + ;; zero and the 8.3 name is derived instead. + lda dos_dirent_longfilename_length + bne gcwd_havesegment + jsr dos_derive_dotted_shortname + lda dos_dirent_longfilename_length +gcwd_havesegment: + sta " + sec + jmp return_from_trap_with_carry_flag -!if DEBUG_HYPPO { -;; tya -;; tax -;; jsr checkpoint_bytetohex -;; sty iv1+0 -;; stx iv1+1 -;; -;; jsr checkpoint -;; .byte 0,"Filename contains $00 @ position $" -;; iv1: .byte "%%",0 -} +gcwd_cwd_to_zptempv32: + ldx #3 +- lda dos_disk_cwd_cluster,x + sta " -trap_dos_mkfile: + jmp return_from_trap_with_success_and_zero_accumulator - ;; XXX Filename must already be set. - ;; XXX Must be a file in the current directory only. - ;; XXX Can only create normal files, not directories - ;; (change attribute after). - ;; XXX Only supports 8.3 names for now. - ;; XXX Filenames without extension might still cause problems. - ;; XXX Allocates 512KB at a time, i.e., a full FAT sector's - ;; worth of clusters. - ;; XXX Allocates a contiguous block, so that D81s etc can - ;; be created, and guaranteed contiguous on the storage, - ;; so that they can be mounted. - ;; XXX Size of file specified in $ZZYYXX, i.e., limit of 16MB. - ;; XXX Doesn't handle full file systems (or ones without enough space - ;; free properly. Should check candidate cluster number is not too - ;; high, and abort if it is. +tdcd1: + +Checkpoint "trap_dos_chdir " - ;; First, make sure the file doesn't already exist + jmp generic_fail_from_error_code + +;; ---- trap_dos_mkdir ---- + +trap_dos_mkdir: + ;; Creates a sub-directory in the current directory. + ;; + ;; Precondition: filename already set via hyppo_setname. + ;; Errors: dos_errorcode_file_exists if a file or sub-directory + ;; already exists with that name. + ;; + ;; Example: + ;; ; Set the new directory's name first via hyppo_setname (A=$2E). + ;; LDA #$0E : STA $D640 : CLV : BCC error + + ;; 1. Existence check (mirrors trap_dos_mkfile). jsr dos_findfile - bcc + - ;; File exists, so abort - clc + bcc mkdir_notfound lda #dos_errorcode_file_exists - sta dos_error_code - jmp return_from_trap_with_failure -+ - - ;; We need 1 FAT sector per 512KB of data. - ;; I.e., shift ZZ right by three bits to get number - ;; of empty FAT sectors we need to indicate sufficient space. - lda hypervisor_z - lsr - lsr - lsr - clc - adc #$01 - sta dos_scratch_byte_1 + +Checkpoint "trap_dos_mkdir " + jmp mkfile_fail_with_a +mkdir_notfound: + + ;; Build the short name and work out how many LFN pieces it needs. + jsr dos_analyze_name_or_fail + + ;; 2. Find a single free cluster for the new directory's own + ;; data. Unlike mkfile (which needs a whole *empty FAT sector* + ;; of contiguous clusters, since files need contiguous storage), + ;; a directory only ever needs exactly one cluster here, so this + ;; scans for the first individually-free cluster anywhere, + ;; rather than requiring 128 consecutive free ones. + ;; + ;; Must start sector-aligned (a multiple of 128), not at cluster + ;; 2: mkdir_found_free_cluster adds the scan's in-sector delta + ;; (0-127) straight onto this value, so starting at 2 would mislabel + ;; every found cluster as 2 higher than the one actually checked/ + ;; allocated. Clusters 0/1's reserved FAT entries are never zero + ;; on a valid volume, so including them in the scan is harmless. + jsr sd_map_sectorbuffer - ;; Now go looking for empty FAT sectors - ;; Start at cluster 128, and add 128 each time to step through - ;; them. - ;; This skips the first sector of FAT, which always has some used - ;; bits, and ensures we can allocate on a whole sector basis. - lda #128 + lda #0 sta <(zptempv32+0) - lda #$00 sta <(zptempv32+1) sta <(zptempv32+2) sta <(zptempv32+3) - ;; Initially 0 empty pages found - lda #0 - sta dos_scratch_byte_2 +mkdir_find_free_cluster_loop: + jsr dos_copy_zptempv32_and_read_fat_sector - jsr sd_map_sectorbuffer + ldx #0 + ldy #0 +mkdir_scan_firsthalf: + lda sd_sectorbuffer,y + ora sd_sectorbuffer+1,y + ora sd_sectorbuffer+2,y + ora sd_sectorbuffer+3,y + beq mkdir_found_free_cluster + inx + iny : iny : iny : iny + cpx #64 + bne mkdir_scan_firsthalf -find_empty_fat_page_loop: + ldy #0 +mkdir_scan_secondhalf: + lda sd_sectorbuffer+$100,y + ora sd_sectorbuffer+$101,y + ora sd_sectorbuffer+$102,y + ora sd_sectorbuffer+$103,y + beq mkdir_found_free_cluster + inx + iny : iny : iny : iny + cpx #128 + bne mkdir_scan_secondhalf - ldx #3 -- lda " + jmp generic_fail_from_error_code + +mkdir_havedirent: + jsr dos_write_lfn_and_shortentry + + ;; dirent: attributes/cluster/length, commit - shared with mkfile. + lda #fs_fat32_attribute_isdirectory + ldx #0 + jsr dos_write_dirent_common + + +Checkpoint "trap_dos_mkdir " + jmp return_from_trap_with_success + +;; Zeroes the whole 512-byte sd_sectorbuffer. +dos_zero_sectorbuffer: + ldy #0 +- lda #0 + sta sd_sectorbuffer,y + sta sd_sectorbuffer+$100,y + iny + bne - + rts + +;; ---- trap_dos_rmdir ---- + +trap_dos_rmdir: + ;; Removes an empty sub-directory from the current directory. + ;; + ;; Precondition: filename already set via hyppo_setname. + ;; Errors: dos_errorcode_not_a_directory if the named entry isn't + ;; a directory; dos_errorcode_directory_not_empty if it contains + ;; anything besides '.' and '..'. + ;; + ;; Example: + ;; ; Set the target directory's name first via hyppo_setname (A=$2E). + ;; LDA #$10 : STA $D640 : CLV : BCC error + jsr dos_findfile + bcs rmdir_gotfile + +Checkpoint "trap_dos_rmdir " + jmp generic_fail_from_error_code + +rmdir_gotfile: + lda dos_dirent_type_and_attribs + and #fs_fat32_attribute_isdirectory + bne rmdir_is_a_directory + lda #dos_errorcode_not_a_directory + +Checkpoint "trap_dos_rmdir " + jmp mkfile_fail_with_a + +rmdir_is_a_directory: + ;; Temporarily point "cwd" at the target directory so the + ;; existing dos_opendir/dos_readdir machinery can scan it for + ;; emptiness. + jsr rmdir_save_cwd + + lda dos_dirent_cluster+0 + sta dos_disk_cwd_cluster+0 + lda dos_dirent_cluster+1 + sta dos_disk_cwd_cluster+1 + lda dos_dirent_cluster+2 + sta dos_disk_cwd_cluster+2 + lda dos_dirent_cluster+3 + sta dos_disk_cwd_cluster+3 + + jsr dos_opendir_save_current_fd + jsr dos_opendir + bcs rmdir_target_opened + jsr dos_opendir_restore_current_fd + jsr rmdir_restore_cwd + +Checkpoint "trap_dos_rmdir " + jmp return_from_trap_with_failure + +rmdir_target_opened: +rmdir_scanloop: + jsr dos_readdir + bcs rmdir_gotentry + + ;; readdir failed: per its documented error table, $85 invalid + ;; cluster means "read past the end of the directory" - i.e. + ;; genuinely empty (not dos_errorcode_eof, which is a different, + ;; file-read-oriented code). Anything else is a real error that + ;; must be propagated (not silently treated as "empty, ok to + ;; delete"). + lda dos_error_code + cmp #dos_errorcode_invalid_cluster + beq rmdir_scan_done + pha + jsr rmdir_close_and_restore + pla + +Checkpoint "trap_dos_rmdir " + jmp mkfile_fail_with_a + +rmdir_gotentry: + ;; dos_dirent_shortfilename is the raw, space-padded 11-byte + ;; on-disk short name, distinct from the reconstructed + ;; dos_dirent_longfilename: '.' is ". " and '..' is + ;; ".. ". + lda dos_dirent_shortfilename+0 + cmp #$2e + bne rmdir_notempty + lda dos_dirent_shortfilename+1 + cmp #$2e + beq rmdir_scanloop + cmp #$20 + beq rmdir_scanloop + +rmdir_notempty: + jsr rmdir_close_and_restore + lda #dos_errorcode_directory_not_empty + +Checkpoint "trap_dos_rmdir " + jmp mkfile_fail_with_a + +rmdir_scan_done: + jsr rmdir_close_and_restore + + ;; Empty - re-find the target in the (now-restored) parent + ;; directory. dos_requested_filename is untouched by the scan + ;; above (dos_readdir doesn't do name matching), so this locates + ;; the same entry and freshly populates dos_direntstart_* ready + ;; for removal. Marking a directory's own dirent deleted and + ;; freeing its cluster chain is mechanically identical to + ;; removing a file's, so this just reuses dos_rmfile directly. + jsr dos_findfile + bcs rmdir_relocated + +Checkpoint "trap_dos_rmdir " + jmp return_from_trap_with_failure + +rmdir_relocated: + jsr dos_rmfile + +Checkpoint "trap_dos_rmdir " + jmp return_from_trap_with_carry_flag + +rmdir_close_and_restore: + jsr dos_closefile_and_restore_current_fd + jmp rmdir_restore_cwd + +;; ---- trap_dos_opendir ---- + +trap_dos_opendir: + + ;; X = File descriptor + ;; Y = Page of memory to write dirent into + + ;; Open the current working directory for iteration. + ;; + jsr dos_opendir + bcs tdod1 + + ;; Something has gone wrong. Assume dos_opendir will + ;; have set error code + ;; +generic_fail_from_error_code: + lda dos_error_code + jmp return_from_trap_with_failure + +tdod1: + ;; Directory opened ok. + ;; + jmp return_from_trap_with_success_and_file_descriptor_in_a + +;; ---- trap_dos_readdir ---- + +trap_dos_readdir: + + ;; Read next directory entry from file descriptor $XX + ;; Return dirent structure to $YY00 + ;; in first 32KB of mapped address space + + +Checkpoint "trap_dos_readdir" + + jsr sd_map_sectorbuffer + + ;; Get offset to current file descriptor + ;; (we can't use X register, as has been clobbered in the jump + ;; table dispatch code) + ;; + ldx hypervisor_x + stx dos_current_file_descriptor + + jsr dos_get_file_descriptor_offset + bcc tdrd1 + sta dos_current_file_descriptor_offset + + jsr dos_readdir + bcc tdrd1 + + ;; Read the directory entry, now copy it to userland + ;; + jsr hypervisor_setup_copy_region + bcc tdrd1 + + ;; We can now copy the bytes of the dirent to user-space + ;; + ldy #dos_dirent_structure_length-1 +tdrd2: + ;; This loop actually copies the whole dirent. + ;; XXX dos_dirent_longfilename must be first in the dirent structure + lda dos_dirent_longfilename,y + sta (" + + jmp return_from_trap_with_success + +;; ======================== + +tdrd1: + +Checkpoint "trap_dos_readdir " + + bra generic_fail_from_error_code + +;; ---- trap_dos_closedir ---- + +trap_dos_closedir: + jmp trap_dos_closefile + +;; ---- trap_dos_openfile ---- + + +trap_dos_openfile: + + ;; Opens file in current dirent structure + ;; XXX - This means we must preserve the dirent struct when + ;; context-switching to avoid a race-condition + + jsr dos_openfile + bcc tdof1 + + +Checkpoint "trap_dos_openfile " + + jmp return_from_trap_with_success_and_file_descriptor_in_a + +tdof1: + +Checkpoint "trap_dos_openfile " + + bra generic_fail_from_error_code + +;; ---- trap_dos_readfile ---- + +trap_dos_readfile: + jsr dos_readfile + jmp return_from_trap_with_carry_flag + +;; ---- trap_dos_writefile ---- + +trap_dos_writefile: + jsr dos_writefile + jmp return_from_trap_with_carry_flag + +;; ---- trap_dos_mkfile ---- + +trap_dos_mkfile: + + ;; XXX Filename must already be set. + ;; XXX Must be a file in the current directory only. + ;; XXX Can only create normal files, not directories + ;; (change attribute after). + ;; XXX Allocates 512KB at a time, i.e., a full FAT sector's + ;; worth of clusters. + ;; XXX Allocates a contiguous block, so that D81s etc can + ;; be created, and guaranteed contiguous on the storage, + ;; so that they can be mounted. + ;; XXX Size of file specified in $ZZYYXX, i.e., limit of 16MB. + ;; XXX Doesn't handle full file systems (or ones without enough space + ;; free properly. Should check candidate cluster number is not too + ;; high, and abort if it is. + + ;; First, make sure the file doesn't already exist + jsr dos_findfile + bcc + + ;; File exists, so abort + lda #dos_errorcode_file_exists + jmp mkfile_fail_with_a ++ + + ;; Build the short name and work out how many LFN pieces (if + ;; any) it needs, before allocating anything. + jsr dos_analyze_name_or_fail + + ;; We need 1 FAT sector per 512KB of data. + ;; I.e., shift ZZ right by three bits to get number + ;; of empty FAT sectors we need to indicate sufficient space. + lda hypervisor_z + lsr + lsr + lsr + clc + adc #$01 + sta " + + jmp return_from_trap_with_success +tdcf1: + +Checkpoint "trap_dos_closefile " + + jmp generic_fail_from_error_code + +;; ---- trap_dos_seekfile ---- + + +;; ======================== + +trap_dos_seekfile: + ;; Seeks to a given sector within the currently open file. + ;; + ;; Precondition: a file is currently open (hyppo_openfile) - same + ;; implicit "current file" convention as hyppo_readfile / + ;; hyppo_writefile (no FD register input; operates on whichever + ;; file was last opened). + ;; + ;; Inputs: X/Y/Z = 24-bit target sector number within the file + ;; (LSB/mid/MSB), matching hyppo_mkfile's $ZZYYXX convention. + ;; + ;; Narrow contract, not a general random-access primitive: FAT32 + ;; cluster chains are singly-linked, so this always walks forward + ;; from the start of the file, sector by sector. There is no + ;; per-file-descriptor length tracking, so this does not + ;; bounds-check against the file's real length - seeking past + ;; the end fails naturally when the cluster chain runs out, + ;; rather than with a dedicated end-of-file error. + ;; + ;; Example: + ;; ; Assume the file is already open (hyppo_openfile). + ;; LDX #$05 ; sector count, LSB + ;; LDY #$00 ; sector count, middle byte + ;; LDZ #$00 ; sector count, MSB + ;; LDA #$24 : STA $D640 : CLV : BCC error + + ;; Save the 24-bit target sector count in zptempv2/zptempp. + stx " + jmp return_from_trap_with_carry_flag + +seekfile_atstart: +seekfile_loop: + lda " + jmp return_from_trap_with_carry_flag + +seekfile_decrement: + lda " + jmp return_from_trap_with_success + +;; ---- trap_dos_rmfile ---- + +trap_dos_rmfile: + jsr dos_rmfile + jmp return_from_trap_with_carry_flag + +;; ---- trap_dos_fstat ---- + +trap_dos_fstat: + ;; Returns raw dirent info (short name, attributes, length, dates) + ;; for the currently-located file. + ;; + ;; Precondition: hyppo_setname + hyppo_findfile first, to make the + ;; target the "current" match. + ;; Y: MSB of destination area for the raw 32-byte dirent. + ;; + ;; Example: + ;; ; Assume setname+findfile already located the target. + ;; LDY #$80 ; destination page for the result + ;; LDA #$28 : STA $D640 : CLV : BCC error + jsr dos_fstat + +Checkpoint "trap_dos_fstat " + jmp return_from_trap_with_carry_flag + +;; ---- trap_dos_rename ---- + +trap_dos_rename: + ;; Renames an already-open file, or the currently-open directory + ;; (an FD from hyppo_opendir - not an arbitrary named + ;; subdirectory). A new name that fits 8.3 is written in place; + ;; one that needs VFAT long-name pieces is relocated to fresh + ;; free slots elsewhere in the directory instead (see + ;; rename_lfn below), reusing the same LFN writer mkfile/mkdir + ;; use. Either way the entry's attributes/dates/cluster/length + ;; are preserved untouched - only the name changes. + ;; + ;; Precondition: like hyppo_writefile, this operates on an + ;; already-open file descriptor: X = the FD (from hyppo_openfile, + ;; or hyppo_opendir to rename the directory itself). Y = page of + ;; a null-terminated buffer holding the new name (same + ;; convention hyppo_setname itself uses for its input). For a + ;; FILE FD specifically, cwd must still be whatever directory + ;; it was opened from - files carry no ".." of their own, so + ;; (unlike a directory FD) there's no way to re-derive their + ;; parent if cwd has since changed. + ;; + ;; Errors: dos_errorcode_invalid_file_descriptor if X isn't an + ;; open FD; dos_errorcode_file_exists if the new name already + ;; exists; dos_errorcode_name_too_long if the new name is empty + ;; or too long to ever reassemble (more than 5 LFN pieces, i.e. + ;; over 65 characters); dos_errorcode_directory_full if it needs + ;; LFN pieces and there's no room left for them; dos_errorcode_ + ;; file_not_found if asked to rename the root directory (it has + ;; no entry of its own to rename), or if the FD's own entry can + ;; no longer be located (e.g. a file whose cwd precondition + ;; above wasn't met). + ;; + ;; Example: + ;; ; Assume the file is already open (hyppo_openfile) with its FD in X. + ;; LDX openfile_fd + ;; LDY #$80 ; page of a nul-terminated new-name buffer + ;; LDA #$2A : STA $D640 : CLV : BCC error + + ;; Pull the new name out of userland into dos_requested_filename, + ;; via the exact same safety-checked path hyppo_setname uses. + jsr dos_setname_from_userspace + lbcc rename_fail_alreadyset +rename_have_name: + + ;; Save the real cwd - restored before every return below (we + ;; temporarily repoint it at the parent directory we need to + ;; search, reusing the existing dos_opendir/dos_readdir + ;; machinery on it, same trick hyppo_getcwd/hyppo_rmdir use). + ldx #3 +- lda dos_disk_cwd_cluster,x + sta " + jmp mkfile_fail_with_a +rename_not_root: + jsr dos_find_parent_of_cluster + +rename_have_parent: + jsr dos_find_dirent_in_cwd_by_cluster + bcs rename_found + pha ;; rename_restore_cwd clobbers A - save the error code + jsr rename_restore_cwd + pla + +Checkpoint "trap_dos_rename " + jmp mkfile_fail_with_a + +rename_found: + ;; Protect our own dirent position before the destination check + ;; below runs its own findfile/readdir scan (which would + ;; otherwise clobber dos_direntstart_*). cwd is still pointed + ;; at our parent, which is exactly where the new name must not + ;; already exist. + jsr dos_save_direntstart_to_zptemp + + jsr dos_findfile + bcc rename_destfree + jsr rename_restore_cwd + lda #dos_errorcode_file_exists + +Checkpoint "trap_dos_rename " + jmp mkfile_fail_with_a + +rename_destfree: + ;; Restore the saved position and locate its sector. + jsr dos_restore_direntstart_from_zptemp + jsr dos_goto_direntstart_and_point_scratch_vector + + jsr dos_analyze_name_for_dirent + bcc rename_shortcopy_setup + + ;; Carry set: either a valid LFN piece count (relocate), or + ;; $ff (name unusable - too long to ever reassemble, or empty). + lda " + jmp return_from_trap_with_success - ;; Work out where it will be in the 2nd FAT - lda dos_disk_table_offset - ora #fs_fat32_length_of_fat - tay +rename_name_too_long: + jsr rename_restore_cwd + lda #dos_errorcode_name_too_long + +Checkpoint "trap_dos_rename " + jmp mkfile_fail_with_a + +rename_fail_alreadyset: + +Checkpoint "trap_dos_rename " + jmp return_from_trap_with_failure + +;; New name doesn't fit 8.3 - relocate to N+1 fresh consecutive slots +;; elsewhere in the directory. dos_write_lfn_and_shortentry only fills +;; in the name fields (offsets 0-10); attributes/dates/cluster/length +;; (offsets 11-31) are saved from the old entry and copied verbatim. +rename_lfn: + ;; dos_analyze_name_for_dirent (just called) may have gone + ;; through dos_shortname_exists to check the new tilde-numbered + ;; short name for collisions, which does its own directory scan + ;; and clobbers the shared SD sector buffer dos_scratch_vector + ;; still points into. dos_direntstart_* itself is untouched by + ;; that scan, so re-derive dos_scratch_vector from it before + ;; reading anything through it. Direct variant - must not + ;; disturb this FD's own currentcluster/sectorincluster (the + ;; caller may keep reading/writing this FD after the rename). + jsr dos_goto_direntstart_direct_and_point_scratch_vector + + ldy #fs_fat32_dirent_offset_attributes ldx #0 -- lda $d681,x - adc dos_disk_table,y +rlfn_save_loop: + lda (" + jmp return_from_trap_with_failure + +rename_lfn_havedirent: + jsr dos_write_lfn_and_shortentry + + ldy #fs_fat32_dirent_offset_attributes + ldx #0 +rlfn_restore_loop: + lda dos_rename_saved_dirent,x + sta (" + !8 0 +} -trap_dos_readdir: + jmp return_from_trap_with_success - ;; Read next directory entry from file descriptor $XX - ;; Return dirent structure to $YY00 - ;; in first 32KB of mapped address space +;; ---- trap_dos_setup_transfer_area ---- - +Checkpoint "trap_dos_readdir" +trap_dos_setup_transfer_area: - jsr sd_map_sectorbuffer + jsr hypervisor_setup_copy_region - ;; Get offset to current file descriptor - ;; (we can't use X register, as has been clobbered in the jump - ;; table dispatch code) - ;; + jmp return_from_trap_with_carry_flag + +;; ---- trap_dos_cdrootdir ---- + +trap_dos_cdrootdir: ldx hypervisor_x - stx dos_current_file_descriptor + jsr dos_cdroot + jmp return_from_trap_with_carry_flag - jsr dos_get_file_descriptor_offset - bcc tdrd1 - sta dos_current_file_descriptor_offset +;; ---- trap_dos_loadfile_attic ---- - jsr dos_readdir - bcc tdrd1 +trap_dos_loadfile_attic: + lda #$08 ; Set address to $8xxxxxx to access attic RAM + !8 $2c ; BIT $xxxx to skip lda #$00 below + ;; FALL THROUGH - ;; Read the directory entry, now copy it to userland - ;; - jsr hypervisor_setup_copy_region - bcc tdrd1 +trap_dos_loadfile: - ;; We can now copy the bytes of the dirent to user-space + ;; Only allow loading into lower 16MB to avoid possibility of writing + ;; over hypervisor ;; - ldy #dos_dirent_structure_length-1 -tdrd2: - ;; This loop actually copies the whole dirent. - ;; XXX dos_dirent_longfilename must be first in the dirent structure - lda dos_dirent_longfilename,y - sta (" + lda hypervisor_x + sta " +trap_dos_d81attach0: - lda dos_error_code - jmp return_from_trap_with_failure + +Checkpoint "trap_dos_d81attach0" -;; ======================== + ldx #$00 + jsr dos_attach + jmp return_from_trap_with_carry_flag -trap_dos_closedir: - jmp trap_dos_closefile +;; ---- trap_dos_d81detach ---- -;; ======================== +trap_dos_d81detach: -trap_dos_readfile: - jsr dos_readfile - jmp return_from_trap_with_carry_flag + +Checkpoint "trap_dos_d81detach" -trap_dos_writefile: - jsr dos_writefile - jmp return_from_trap_with_carry_flag + ldx #%11000010 ;; detach both drives, don't attach real drives + jsr dos_attach -trap_dos_rmfile: - jsr dos_rmfile jmp return_from_trap_with_carry_flag -trap_dos_fstat: - jsr dos_fstat - jmp return_from_trap_with_carry_flag +;; ---- trap_dos_d81write_en ---- -;; ======================== +trap_dos_d81write_en: -trap_dos_cdrootdir: - ldx hypervisor_x - jsr dos_cdroot + jsr dos_d81write_en jmp return_from_trap_with_carry_flag -trap_dos_chdir: +dos_d81write_en: + lda $d68b + and #$03 + cmp #$03 + bne td81we1 + ora #$04 + sta $d68b - ;; Opens file in current dirent structure - ;; XXX - This means we must preserve the dirent struct when - ;; context-switching to avoid a race-condition + ;; Mark disk image write-enabled in proces descriptor + lda currenttask_d81_image0_flags + ora #d81_image_flag_write_en - jsr dos_chdir - bcc tdcd1 + sec + rts - +Checkpoint "trap_dos_chdir " +td81we1: + ;; No disk image mounted + ;; - jmp return_from_trap_with_success_and_zero_accumulator + +Checkpoint "dos_d81writ_en-FAIL" -tdcd1: - +Checkpoint "trap_dos_chdir " + lda #dos_errorcode_no_such_disk + sta dos_error_code + clc + rts - lda dos_error_code - jmp return_from_trap_with_failure +;; ---- trap_dos_d81attach1 ---- -;; ======================== +trap_dos_d81attach1: + +Checkpoint "trap_dos_d81attach1" -trap_dos_openfile: + ldx #$01 + jsr dos_attach + jmp return_from_trap_with_carry_flag - ;; Opens file in current dirent structure - ;; XXX - This means we must preserve the dirent struct when - ;; context-switching to avoid a race-condition +;; ---- trap_dos_get_proc_desc ---- - jsr dos_openfile - bcc tdof1 +trap_dos_get_proc_desc: + jsr hypervisor_setup_copy_region + bcc @bad + ldy #0 +@copyloop: + lda currenttask_block,y + sta (" +;; ---- trap_dos_attach ---- - jmp return_from_trap_with_success_and_file_descriptor_in_a +trap_dos_attach: -tdof1: - +Checkpoint "trap_dos_openfile " + +Checkpoint "trap_dos_attach" - lda dos_error_code - jmp return_from_trap_with_failure + ldx hypervisor_x + jsr dos_attach -;; ======================== + jmp return_from_trap_with_carry_flag -trap_dos_closefile: +;; ---- trap_dos_filedate ---- - ldx hypervisor_x - stx dos_current_file_descriptor +;; BG: the following are placeholders for the future development - jsr dos_get_file_descriptor_offset - bcc tdcf1 - sta dos_current_file_descriptor_offset - jsr dos_closefile - bcc tdcf1 +trap_dos_filedate: +trap_dos_gettasklist: +trap_dos_sendmessage: +trap_dos_receivemessage: +trap_dos_writeintotask: +trap_dos_readoutoftask: +trap_dos_terminateothertask: +trap_dos_create_task_native: +trap_dos_load_into_task: +trap_dos_create_task_c64: +trap_dos_create_task_c65: +trap_dos_exit_and_switch_to_task: +trap_dos_switch_to_task: +trap_dos_exit_task: - +Checkpoint "trap_dos_closefile " + jmp invalid_subfunction;; + +;; ---- trap_task_toggle_rom_writeprotect ---- +trap_task_toggle_rom_writeprotect: + lda hypervisor_feature_enables + eor #$04 + sta hypervisor_feature_enables +returnFeatureState: + ;; Pass updated state back out to caller, so they know the result + sta hypervisor_a jmp return_from_trap_with_success -tdcf1: - +Checkpoint "trap_dos_closefile " - lda dos_error_code - jmp return_from_trap_with_failure +;; ---- trap_task_toggle_force_4502 ---- -;; ======================== +trap_task_toggle_force_4502: + lda hypervisor_feature_enables + eor #$20 + sta hypervisor_feature_enables + bra returnFeatureState -trap_dos_findfile: +;; ---- trap_task_get_mapping ---- - jsr dos_findfile +trap_task_get_mapping: + jsr hypervisor_setup_copy_region + bcc @bad + ldy #5 +@copyloop: + lda hypervisor_maplohi,y + sta (" - !8 0 -} +;; 0 writes hypervisor_x/y/z (mkfile). +dos_write_dirent_common: + ldy #fs_fat32_dirent_offset_attributes + sta (= 9 + lda = 4 -dos_clear_filedescriptors: + lda <(zptempv2+1) + jsr ran_copy_base_chars ;; base_len already <= 8 here + lda done + inc last piece + sec + sbc #13 + bra ran_tilde_piece_loop +ran_tilde_piece_lastdone: + lda #0 + bra ran_tilde_piece_loop +ran_tilde_piece_done: sec rts -;; ======================== +;; A = number of base chars to copy. Copies+uppercases into +;; dos_dirent_shortfilename, leaves X = count. +ran_copy_base_chars: + sta <(zptempv2+1) ;; loop bound + ldx #0 +ran_copy_base_chars_loop: + cpx <(zptempv2+1) + beq ran_copy_base_chars_done + lda dos_requested_filename,x + jsr toupper + sta dos_dirent_shortfilename,x + inx + bra ran_copy_base_chars_loop +ran_copy_base_chars_done: + rts + +;; A = number of extension chars to copy (0 is a valid no-op). +;; Copies+uppercases into dos_dirent_shortfilename+8. +ran_copy_ext_chars: + sta must be LFN + bra drce_longname ;; MATCH -> must be LFN drce_cont0: tya ;; from safe keeping @@ -2642,7 +4246,7 @@ drce_cont0: and #$08 cmp #$08 ;; %00001000 Vol-ID bne drce_cont2 - jmp drce_cont_next_part ; Ignore it + bra drce_cont_next_part ; Ignore it drce_cont2: tya ;; from safe keeping @@ -2668,12 +4272,12 @@ drce_cont2: drce_cont3: ;; was not hidden/system, or Vol-ID, or LFN, or was special case "..", ;; so we process this entry regardless of if read-only (bit0) or not - jmp drce_normalrecord + bra drce_normalrecord drce_ignore: ;; Ignore hidden/system files for now ;; XXX We should have a flag to enable/disable this behaviour - jmp drce_cont_next_part + bra drce_cont_next_part dotdotshortname: !text ".. " @@ -2683,21 +4287,19 @@ dotdotshortname: drce_longname: disable_lfn_byte: - jmp drce_cont_next_part + jmp drce_cont_next_part ;; First byte (jmp) self modifying fom syspart.asm. JMP = LFN disabled, BIT= LFN Enabled ;; make sure long entry type is "filename" (=$00) ;; ldy #fs_fat32_dirent_offset_lfn_type lda (msg_clusternumber jsr printmessage ldy #$00 - ldz dos_current_cluster+3 + ldz $0200 - lda dos_bytes_remaining+2 - ora dos_bytes_remaining+3 + lda regular data file - lda dos_file_descriptors+2,y - and dos_file_descriptors+1,y - cmp #$ff - bne drf_ok ; not $ffff -> regular data file - lda dos_file_descriptors,y - and #$f0 - cmp #$f0 - beq drf_eof ; $0ffffff? -> eof marker - -drf_ok: - jsr drf_clearwriteandincreasesector - sec - rts - -drf_eof: + pha ;; 0=ok, 1=eof - remember across the clearwrite call jsr drf_clearwriteandincreasesector + pla + beq drf_ok2 clc rts +drf_ok2: + sec + rts drf_fail: clc rts drf_clearwriteandincreasesector - lda dos_scratch_byte_1 + lda $6200 + sta $0ffd + sta freeze_region_dmalist + sta $d701 + lda #freeze_region_dmalist - sta $d701 - lda #freeze_region_dmalist - sta $d701 - lda #$6200 - sta $0FFD - sta $1000 - sta $2640 - sta zptempv32+1 - lda #<$0FFD - sta zptempv32+2 - lda #>$0FFD - sta zptempv32+3 - - ;; First, make sure the read pointer is at the start of the thumbnail - ldz #$00 - ;; Then advance pointer address to $D641 - lda [$6200 - sta $0ffd - sta freeze_region_dmalist - sta $d701 - lda #$6e11 - sta $0ffd - sta msg_hyppohelpfirst jsr printmessage - jmp first_boot_flag_instruction + bra first_boot_flag_instruction not_first_boot_message: ldx #') to skip -; over the loop and then comfortably step through early -; hypervisor code. + ;; add a test if the ESC key is held down + ;; if so, make an endless loop so that a person debugging + ;; and turn trace mode on, move the pc (with 'g') to skip + ;; over the loop and then comfortably step through early + ;; hypervisor code. ldx #$ff ;; make a few attempts are reading keyscan early @earlyscan: @@ -795,7 +775,7 @@ morewaiting: plx - jmp tryreadmbr + bra tryreadmbr trybus0: dex bne morewaiting @@ -839,8 +819,7 @@ tryreadmbr: jsr scankeyboard bcs nokey2 cmp #$20 - bne nokey2 - jmp utility_menu + lbeq utility_menu nokey2: ;; Oops, cant read MBR @@ -859,7 +838,7 @@ nokey2: ;; +Checkpoint "re-try reading MBR of sdcard" - jmp tryreadmbr + bra tryreadmbr ;; ======================== @@ -895,8 +874,7 @@ gotmbr: ;; If we have no disks, offer the utility menu lda dos_disk_count - bne @thereIsADisk - jmp utility_menu + lbeq utility_menu @thereIsADisk: ;; Go to root directory on default disk @@ -1371,7 +1349,7 @@ loadrom: jsr attempt_loadcharrom bcs loadedcharromok - jmp loadc65rom + bra loadc65rom ;; ======================== @@ -1444,7 +1422,7 @@ loadedok: cmp #$00 bne @romFileNotTooShort @romFileIsTooShort: - jmp romfiletooshort + bra romfiletooshort @romFileNotTooShort: cmp #$01 beq @romFileIsTooShort @@ -1453,7 +1431,7 @@ loadedok: lda file_pagesread beq @romFileNotTooLong @romFileIsTooLong: - jmp romfiletoolong + bra romfiletoolong @romFileNotTooLong: ;; the loaded ROM was OK in size @@ -1472,7 +1450,7 @@ loadedok: lda # carry + lda #$4c ; JMP $xxxx opcode (LFN support disabled) + bcc @setlfn + lda #$2c ; BIT $xxxx opcode (LFN support enabled) +@setlfn: sta disable_lfn_byte -@nolfn: ;; Copy MAC address ldx #$05 maccopy: diff --git a/src/hyppo/targetsetup.asm b/src/hyppo/targetsetup.asm index 162b7cacc..7bde90051 100644 --- a/src/hyppo/targetsetup.asm +++ b/src/hyppo/targetsetup.asm @@ -68,7 +68,7 @@ mps3_loop: dex bne - - jmp mps3_loop + bra mps3_loop i2c_job_delay: phx @@ -124,7 +124,7 @@ mps_loop: bne - - jmp mps_loop + bra mps_loop megaphone_r1_i2c_settings: diff --git a/src/hyppo/task.asm b/src/hyppo/task.asm index baa9890e4..ff1e20df3 100644 --- a/src/hyppo/task.asm +++ b/src/hyppo/task.asm @@ -168,8 +168,7 @@ task_asblankslate: jsr task_new_processcontrolblock - jsr task_set_c64_memorymap - rts + jmp task_set_c64_memorymap task_set_as_system_task: ;; Task ID is reserved for the hypervisor and its helpers, and prevents freezing @@ -238,7 +237,7 @@ ethernet_remote_trap: lda #>$8000 sta hypervisor_pch - jmp safe_exit_to_loaded_program + bra safe_exit_to_loaded_program unstable_illegal_opcode_trap: kill_opcode_trap: @@ -261,9 +260,14 @@ restore_press_trap: non_hypervisor_task: + ;; Carry the task's current directory into the process descriptor + ;; before the freeze writes that page out. + jsr dos_save_cwd_to_task + ;; Clear colour RAM at $DC00 flag, as it causes no end of trouble lda #$01 trb $D030 + ;; and DMA audio lda #$00 sta $d711 diff --git a/src/tools/hyppotest.c b/src/tools/hyppotest.c index 935043ea1..160211f27 100644 --- a/src/tools/hyppotest.c +++ b/src/tools/hyppotest.c @@ -198,7 +198,7 @@ void disassemble_rel8(FILE *f, struct instruction_log *log) void disassemble_rel16(FILE *f, struct instruction_log *log) { - fprintf(f, "$%04X", log->pc + 2 + rel16_delta(log->bytes[1] + (log->bytes[1] << 8))); + fprintf(f, "$%04X", log->pc + 3 + rel16_delta(log->bytes[1] + (log->bytes[2] << 8))); } void disassemble_imm(FILE *f, struct instruction_log *log) @@ -607,6 +607,10 @@ void disassemble_instruction(FILE *f, struct instruction_log *log) fprintf(f, "EOR "); disassemble_izpz(f, log); break; + case 0x53: + fprintf(f, "BVC "); + disassemble_rel16(f, log); + break; case 0x56: fprintf(f, "LSR "); disassemble_zpx(f, log); @@ -735,6 +739,10 @@ void disassemble_instruction(FILE *f, struct instruction_log *log) fprintf(f, "ADC "); disassemble_izpz(f, log); break; + case 0x73: + fprintf(f, "BVS "); + disassemble_rel16(f, log); + break; case 0x74: fprintf(f, "STZ "); disassemble_zpx(f, log); @@ -966,6 +974,10 @@ void disassemble_instruction(FILE *f, struct instruction_log *log) fprintf(f, "LDA "); disassemble_izpz(f, log); break; + case 0xb3: + fprintf(f, "BCS "); + disassemble_rel16(f, log); + break; case 0xb4: fprintf(f, "LDY "); disassemble_zpx(f, log); @@ -1138,6 +1150,10 @@ void disassemble_instruction(FILE *f, struct instruction_log *log) fprintf(f, "SBC "); disassemble_imm(f, log); break; + case 0xd3: + fprintf(f, "BNE "); + disassemble_rel16(f, log); + break; case 0xea: fprintf(f, "EOM"); break; @@ -2602,7 +2618,7 @@ bool execute_instruction(struct cpu *cpu, struct instruction_log *log) if (cpu->regs.flags & FLAG_N) cpu->regs.pc += 3; else - cpu->regs.pc += 2 + rel16_delta(log->bytes[1]); + cpu->regs.pc += 3 + rel16_delta(log->bytes[1] + (log->bytes[2] << 8)); break; case 0x14: // TRB $xx log->len = 2; @@ -2831,7 +2847,7 @@ bool execute_instruction(struct cpu *cpu, struct instruction_log *log) if (!(cpu->regs.flags & FLAG_N)) cpu->regs.pc += 3; else - cpu->regs.pc += 2 + rel16_delta(log->bytes[1]); + cpu->regs.pc += 3 + rel16_delta(log->bytes[1] + (log->bytes[2] << 8)); break; case 0x34: // BIT $xx,X log->len = 2; @@ -3029,6 +3045,13 @@ bool execute_instruction(struct cpu *cpu, struct instruction_log *log) log->len = 2; cpu->regs.pc += 2; break; + case 0x53: // BVC $rrrr + log->len = 3; + if (!(cpu->regs.flags & FLAG_V)) + cpu->regs.pc += 3 + rel16_delta(log->bytes[1] + (log->bytes[2] << 8)); + else + cpu->regs.pc += 3; + break; case 0x55: // EOR $nn,X v = read_memory(cpu, addr_zpx(cpu, log)); v ^= cpu->regs.a; @@ -3234,6 +3257,13 @@ bool execute_instruction(struct cpu *cpu, struct instruction_log *log) log->len = 2; cpu->regs.pc += 2; break; + case 0x73: // BVS $rrrr + log->len = 3; + if ((cpu->regs.flags & FLAG_V)) + cpu->regs.pc += 3 + rel16_delta(log->bytes[1] + (log->bytes[2] << 8)); + else + cpu->regs.pc += 3; + break; case 0x74: // STZ $xx,X log->len = 2; cpu->regs.pc += 2; @@ -3322,7 +3352,7 @@ bool execute_instruction(struct cpu *cpu, struct instruction_log *log) break; case 0x83: // BRA $rrrr log->len = 3; - cpu->regs.pc += 2 + rel16_delta(log->bytes[1]); + cpu->regs.pc += 3 + rel16_delta(log->bytes[1] + (log->bytes[2] << 8)); break; case 0x84: // STY $xx log->len = 2; @@ -3597,6 +3627,13 @@ bool execute_instruction(struct cpu *cpu, struct instruction_log *log) cpu->regs.a = read_memory(cpu, addr_izpz(cpu, log)); update_nz(cpu->regs.a); break; + case 0xb3: // BCS $rrrr + log->len = 3; + if ((cpu->regs.flags & FLAG_C)) + cpu->regs.pc += 3 + rel16_delta(log->bytes[1] + (log->bytes[2] << 8)); + else + cpu->regs.pc += 3; + break; case 0xb4: // LDY $xx,X log->len = 2; cpu->regs.pc += 2; @@ -3836,6 +3873,13 @@ bool execute_instruction(struct cpu *cpu, struct instruction_log *log) log->len = 2; cpu->regs.pc += 2; break; + case 0xd3: // BNE $rrrr + log->len = 3; + if (!(cpu->regs.flags & FLAG_Z)) + cpu->regs.pc += 3 + rel16_delta(log->bytes[1] + (log->bytes[2] << 8)); + else + cpu->regs.pc += 3; + break; case 0xe1: // SBC ($nn,X) sbc(cpu, read_memory(cpu, addr_izpx(cpu, log))); log->len = 2; @@ -3930,7 +3974,7 @@ bool execute_instruction(struct cpu *cpu, struct instruction_log *log) case 0xf3: // BEQ $rrrr log->len = 3; if (cpu->regs.flags & FLAG_Z) - cpu->regs.pc += 3 + rel16_delta(log->bytes[1]); + cpu->regs.pc += 3 + rel16_delta(log->bytes[1] + (log->bytes[2] << 8)); else cpu->regs.pc += 3; break; From 0704d9b8514a6886e5ed69a461bf54c97128a3a3 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Mon, 7 Sep 2026 18:43:52 +0200 Subject: [PATCH 2/3] hyppo: stop mkfile's cluster search at the end of the file system trap_dos_mkfile stepped through the FAT 128 clusters at a time with nothing stopping it at the end of the file system, so on a full card it read past the FAT, accepted whatever happened to look blank, and allocated clusters that do not exist. Writing to such a file put data outside the file system. Compare the cursor against the disk's cluster count and fail with dos_errorcode_no_space. The compare is a full 32-bit one: the search moves 128 clusters at a time and can step over the last one rather than landing on it. Test included. --- src/hyppo/dos.asm | 34 +++++++++++++++++++++++++++++++++- src/hyppo/hyppo.test | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/hyppo/dos.asm b/src/hyppo/dos.asm index 6089fa275..c21008d36 100644 --- a/src/hyppo/dos.asm +++ b/src/hyppo/dos.asm @@ -987,7 +987,39 @@ fat_sector_is_empty: lda #$80 jsr dos_add_a_to_zptempv32 - ;; XXX Check that we haven't hit the end of the file system +mkfile_check_end_of_fs: + ;; Stop when the search reaches the end of the file system. + ;; + ;; Without this the loop runs off the end of the FAT, reads + ;; whatever follows it, accepts the first sectors that happen to + ;; read as blank, and allocates clusters that do not exist. The + ;; file is then created at its full length with a chain that + ;; cannot be attached, and writing to it puts data outside the + ;; file system altogether - so the failure is silent at creation + ;; and destructive afterwards. + ;; + ;; Unsigned 32-bit compare: cluster >= cluster_count means we are + ;; past the end. Testing equality alone, as the check in + ;; dos_write.asm does, is not enough here because this walks 128 + ;; clusters at a stride and can step straight over the last one. + ;; + ;; dos_disk_table_offset is already valid: dos_findfile above and + ;; read_fat_sector_for_cluster in the loop both work through the + ;; current disk. + ldx dos_disk_table_offset + sec + lda <(zptempv32+0) + sbc dos_disk_table + fs_fat32_cluster_count + 0,x + lda <(zptempv32+1) + sbc dos_disk_table + fs_fat32_cluster_count + 1,x + lda <(zptempv32+2) + sbc dos_disk_table + fs_fat32_cluster_count + 2,x + lda <(zptempv32+3) + sbc dos_disk_table + fs_fat32_cluster_count + 3,x + bcc + + lda #dos_errorcode_no_space + jmp mkfile_fail_with_a ++ bra find_empty_fat_page_loop diff --git a/src/hyppo/hyppo.test b/src/hyppo/hyppo.test index 10d7cfdcc..b187c68a6 100644 --- a/src/hyppo/hyppo.test +++ b/src/hyppo/hyppo.test @@ -183,3 +183,47 @@ check registers dump instructions 1587000 to 1588000 test end + +#################################################################### +# +# trap_dos_mkfile searches for free space by stepping through the FAT +# 128 clusters at a time. It used to do so with no upper bound - the +# source carried an "XXX Check that we haven't hit the end of the file +# system" where the test should have been - so on a card without a big +# enough run of free space it walked off the end of the FAT, read +# whatever followed, accepted the first sectors that happened to be +# blank, and allocated clusters that do not exist. The file came out at +# its full length with a chain that cannot be attached, and writing to +# it put data outside the file system. +# +# The test enters at the bounds check itself with the search cursor +# already past the last cluster. Hyppo keeps its zero page at $BFxx, +# hence the B register. +# +# Note "ignore all regs": check registers compares every register, so +# without it the unmentioned ones mismatch and the test fails. And the +# check line is required - with expect but no check, the expectations are +# never compared and the test passes whatever happens. Both were verified +# by giving this test a deliberately wrong expectation and confirming it +# then fails. +# +#################################################################### +test "mkfile stops its cluster search at the end of the file system" +loadhyppo bin/HICKUP.M65 +loadhypposymbols src/hyppo/HICKUP.sym +log on failure +let b = $bf +# First disk in the table, cluster count $00001000 +poke dos_disk_table_offset $00 +poke $BB12 $00 $10 $00 $00 +# Cursor has stepped past the last cluster +poke zptempv32 $80 $10 $00 $00 +breakpoint mkfile_fail_with_a +breakpoint find_empty_fat_page_loop +jmp mkfile_check_end_of_fs +ignore all regs +expect pc = mkfile_fail_with_a +# A carries the error code into mkfile_fail_with_a +expect a = $8c +check registers +test end From ded9e42b4c56a083c346d20d3e2e313dfb02f0c1 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Mon, 7 Sep 2026 18:47:04 +0200 Subject: [PATCH 3/3] hyppo: cut mkfile's cluster chain to the file's actual length mkfile allocates whole FAT sectors (128 clusters) and ran the chain out to the end of the last one, so a file's chain was always longer than its recorded length. dos_checkimage wants exactly ceil(size / cluster bytes), so a .D81 created through the trap could never be attached - 200 clusters needed, 256 written. Work the count out up front and stop the chain there; clusters past the end stay free. The count also gives the search an exact FAT sector count, replacing an estimate that was only right at 8 sectors per cluster. Falling out of that: the search records where a free run started instead of winding back to it, and the chain loop walks the sector by pointer. Both have their own zero page bytes: mkfile's dirent-sector save is four bytes in the two-byte zptempv2, so it already overlaps zptempp. Net 20 bytes recovered; getdisksize copies its fields as loops now, so its output order follows the disk table's. hyppotest had no INW/DEW and stopped at the first one. Added, with two fixes it exposed: cpu_stash_ram did not snapshot colour RAM or $FFDxxxx, and stack_pop_ext wrote pop_blame[] unbounded. Tested on hardware: an 819200-byte .D81 created through the trap now attaches. --- src/hyppo/dos.asm | 295 +++++++++++++++++++++++++----------------- src/hyppo/hyppo.test | 58 ++++++++- src/hyppo/main.asm | 21 +++ src/tools/hyppotest.c | 39 +++++- 4 files changed, 292 insertions(+), 121 deletions(-) diff --git a/src/hyppo/dos.asm b/src/hyppo/dos.asm index c21008d36..38638b577 100644 --- a/src/hyppo/dos.asm +++ b/src/hyppo/dos.asm @@ -207,8 +207,12 @@ trap_dos_getdisksize: ;; Y: MSB of destination area (same convention as get_proc_desc). ;; Output, starting at $YY00: ;; $00 dword total sector count of the partition - ;; $04 byte sectors per cluster - ;; $05 dword total cluster count of the partition + ;; $04 dword total cluster count of the partition + ;; $08 byte sectors per cluster + ;; + ;; The order follows dos_disk_table's own field order, so that + ;; each run of fields copies out as a loop rather than as a + ;; hand-unrolled sequence. ;; ;; Example: ;; LDY #$80 ; destination page for the result @@ -221,35 +225,23 @@ gds_havearea: ldx dos_disk_table_offset ldy #0 - - lda dos_disk_table + fs_sector_count + 0,x - sta (" sec @@ -901,15 +893,13 @@ trap_dos_mkfile: ;; XXX Must be a file in the current directory only. ;; XXX Can only create normal files, not directories ;; (change attribute after). - ;; XXX Allocates 512KB at a time, i.e., a full FAT sector's - ;; worth of clusters. - ;; XXX Allocates a contiguous block, so that D81s etc can - ;; be created, and guaranteed contiguous on the storage, - ;; so that they can be mounted. ;; XXX Size of file specified in $ZZYYXX, i.e., limit of 16MB. - ;; XXX Doesn't handle full file systems (or ones without enough space - ;; free properly. Should check candidate cluster number is not too - ;; high, and abort if it is. + ;; + ;; Searches in units of a whole FAT sector - 128 clusters - so + ;; that the space it finds is contiguous, which is what lets a + ;; D81 created here be mounted. The chain it then writes stops at + ;; the exact number of clusters the file needs, leaving the rest + ;; of the last sector free. ;; First, make sure the file doesn't already exist jsr dos_findfile @@ -923,15 +913,84 @@ trap_dos_mkfile: ;; any) it needs, before allocating anything. jsr dos_analyze_name_or_fail - ;; We need 1 FAT sector per 512KB of data. - ;; I.e., shift ZZ right by three bits to get number - ;; of empty FAT sectors we need to indicate sufficient space. + ;; How many clusters the file actually needs. + ;; + ;; The search below works a whole FAT sector at a time, but the + ;; chain written afterwards has to stop at exactly this many + ;; clusters. A chain longer than the file's recorded length is + ;; malformed FAT32 in its own right, and dos_checkimage rejects + ;; it outright, so a D81 created with a rounded-up chain could + ;; never be attached. + ;; + ;; clusters = ceil(ceil(size / 512) / sectors_per_cluster). + ;; Nested round-ups compose, so this is the same answer as + ;; dividing once by the cluster size in bytes, and it never has + ;; to hold anything wider than 16 bits or double + ;; sectors_per_cluster (which is a byte, and can be 128). +mkfile_count_clusters: + ldx dos_disk_table_offset + + ;; 256-byte pages, rounded up: the size's own low byte only + ;; matters as "is there a part page as well", which cmp #1 + ;; turns into a carry. + lda hypervisor_x + cmp #1 + lda hypervisor_y + adc #0 + sta > 7 + rol sta sd_sectorbuffer + sta (sd_sectorbuffer+$200) + bne mkfile_fat_entry + + ;; Sector full: write it to FAT1, mirror to FAT2, and carry the + ;; chain on into the next one. + jsr dos_write_sector_and_fat2_mirror + bra mkfile_fat_write_loop + +mkfile_fat_end_of_chain: + ;; $0FFFFFF8 marks the end of the file. zptempv32 is the running + ;; cluster number, but we are done with it - it gets reloaded + ;; from mkfile_start_cluster below. lda #$F8 - sta $dffc + sta <(zptempv32+0) lda #$FF - sta $dffd - sta $dffe + sta <(zptempv32+1) + sta <(zptempv32+2) lda #$0F - sta $dfff -+ - ;; Write FAT sector to FAT1, then mirror to FAT2. + sta <(zptempv32+3) + jsr mkfile_put_fat_entry jsr dos_write_sector_and_fat2_mirror - ;; More FAT sectors to go? - dec stack_underflow = true; cpu->regs.sp = new_sp; } - log->pop_blame[log->pops++] = memory_blame(cpu, cpu->regs.sp); + // Only the first two entries are ever read back (to blame whoever + // pushed the value), and log->pops is never reset, so a log entry + // reused by a long-running loop would run off the end of the array + // and corrupt the heap. + if (log->pops < MAX_POPS) + log->pop_blame[log->pops++] = memory_blame(cpu, cpu->regs.sp); return read_memory(cpu, cpu->regs.sp); } @@ -3725,6 +3743,23 @@ bool execute_instruction(struct cpu *cpu, struct instruction_log *log) log->len = 2; cpu->regs.pc += 2; break; + case 0xC3: // DEW $nn + case 0xE3: // INW $nn + // 16-bit read-modify-write of a zero page word. N and Z come from + // the whole 16-bit result rather than from either byte, which is + // what makes "dew" followed by "beq" a 16-bit countdown. + log->len = 2; + cpu->regs.pc += 2; + { + unsigned int zpw = addr_zp(cpu, log); + unsigned int w = read_memory(cpu, zpw) | (read_memory(cpu, zpw + 1) << 8); + w = (w + (log->bytes[0] == 0xE3 ? 1 : -1)) & 0xffff; + MEM_WRITE16(cpu, zpw, w & 0xff); + MEM_WRITE16(cpu, zpw + 1, w >> 8); + cpu->regs.flag_n = (w & 0x8000) ? 1 : 0; + cpu->regs.flag_z = (w == 0); + } + break; case 0xC6: // DEC $xx log->len = 2; cpu->regs.pc += 2;