From 8c047bcfce064bba04dfe5038e175c2050bea34e Mon Sep 17 00:00:00 2001 From: cornelk Date: Thu, 12 Mar 2026 19:53:03 -0600 Subject: [PATCH] add split-bank output mode with per-bank file writing --- internal/assembler/asm6/file.go | 114 ++++++++++++------------- internal/assembler/ca65/config.go | 6 +- internal/assembler/ca65/file.go | 128 +++++++++++++++++++---------- internal/assembler/nesasm/file.go | 84 +++++++++++-------- internal/assembler/retroasm/nes.go | 60 +++++++++++--- internal/cli/cli.go | 1 + internal/disasm/disasm_test.go | 14 +++- internal/options/options.go | 3 + internal/pipeline/pipeline.go | 27 +++++- internal/program/prg.go | 3 +- internal/writer/writer.go | 31 ++++--- internal/writer/writer_test.go | 41 +++++++++ 12 files changed, 345 insertions(+), 167 deletions(-) create mode 100644 internal/writer/writer_test.go diff --git a/internal/assembler/asm6/file.go b/internal/assembler/asm6/file.go index a3ff5ca..f3ae678 100644 --- a/internal/assembler/asm6/file.go +++ b/internal/assembler/asm6/file.go @@ -3,6 +3,7 @@ package asm6 import ( "fmt" "io" + "strings" "github.com/retroenv/retrodisasm/internal/assembler" "github.com/retroenv/retrodisasm/internal/options" @@ -30,9 +31,10 @@ type headerByteWrite struct { } type prgBankWrite struct { - address string - bank *program.PRGBank - lastBank bool + address string + bank *program.PRGBank + lastBank bool + firstBank bool } type customWrite func() error @@ -90,9 +92,10 @@ func (f FileWriter) Write() error { lastBank := i == len(f.app.PRG)-1 writes = append(writes, prgBankWrite{ - address: fmt.Sprintf("$%04x", f.app.CodeBaseAddress), - bank: bank, - lastBank: lastBank, + address: fmt.Sprintf("$%04x", bank.BaseAddress), + bank: bank, + lastBank: lastBank, + firstBank: i == 0, }, ) } @@ -129,59 +132,61 @@ func (f FileWriter) Write() error { } func (f FileWriter) writeBank(w prgBankWrite) error { - if err := f.writeSegment(w.address); err != nil { - return err + bankWriteCloser, err := f.newBankWriter(w.bank.Name) + if err != nil { + return fmt.Errorf("creating bank writer: %w", err) + } + defer func() { _ = bankWriteCloser.Close() }() + + if f.options.SplitBanks { + if w.firstBank { + if _, err := fmt.Fprintln(f.mainWriter); err != nil { + return fmt.Errorf("writing newline: %w", err) + } + } + bankFile := bankFilename(f.options.OutputFilename, w.bank.Name) + if _, err := fmt.Fprintf(f.mainWriter, ".include \"%s\"\n", bankFile); err != nil { + return fmt.Errorf("writing include directive: %w", err) + } + } + + bankW := writer.New(f.app, bankWriteCloser, writer.Options{ + OffsetComments: f.options.OffsetComments, + }) + + if _, err := fmt.Fprintf(bankWriteCloser, "\n.base %s\n\n", w.address); err != nil { + return fmt.Errorf("writing segment: %w", err) } - if err := f.writeConstants(w.bank); err != nil { - return err + + if err := bankW.OutputAliasMap(w.bank.Constants); err != nil { + return fmt.Errorf("writing constants output alias map: %w", err) } - if err := f.writeVariables(w.bank); err != nil { - return err + if err := bankW.OutputAliasMap(w.bank.Variables); err != nil { + return fmt.Errorf("writing variables output alias map: %w", err) } - if err := f.writeCode(w.bank); err != nil { - return err + + endIndex := w.bank.LastNonZeroByte(f.options) + if err := bankW.ProcessPRG(w.bank, endIndex); err != nil { + return fmt.Errorf("writing PRG: %w", err) } + vectorsAddr := w.bank.BaseAddress + uint16(len(w.bank.Offsets)) - 6 + if w.lastBank { - if err := f.writeVectors(f.app.Handlers.NMI, f.app.Handlers.Reset, f.app.Handlers.IRQ); err != nil { + if err := f.writeVectorsTo(bankWriteCloser, vectorsAddr, f.app.Handlers.NMI, f.app.Handlers.Reset, f.app.Handlers.IRQ); err != nil { return err } } else { nmi := fmt.Sprintf("$%04X", w.bank.Vectors[0]) reset := fmt.Sprintf("$%04X", w.bank.Vectors[1]) irq := fmt.Sprintf("$%04X", w.bank.Vectors[2]) - if err := f.writeVectors(nmi, reset, irq); err != nil { + if err := f.writeVectorsTo(bankWriteCloser, vectorsAddr, nmi, reset, irq); err != nil { return err } } return nil } -// writeSegment writes a segment header to the output. -func (f FileWriter) writeSegment(address string) error { - _, err := fmt.Fprintf(f.mainWriter, "\n.base %s\n\n", address) - if err != nil { - return fmt.Errorf("writing segment: %w", err) - } - return nil -} - -// writeConstants writes constant aliases to the output. -func (f FileWriter) writeConstants(bank *program.PRGBank) error { - if err := f.writer.OutputAliasMap(bank.Constants); err != nil { - return fmt.Errorf("writing constants output alias map: %w", err) - } - return nil -} - -// writeVariables writes variable aliases to the output. -func (f FileWriter) writeVariables(bank *program.PRGBank) error { - if err := f.writer.OutputAliasMap(bank.Variables); err != nil { - return fmt.Errorf("writing variables output alias map: %w", err) - } - return nil -} - // writeCHR writes the CHR content to the output. func (f FileWriter) writeCHR() error { if f.options.ZeroBytes { @@ -206,33 +211,32 @@ func (f FileWriter) writeCHR() error { return nil } -// writeVectors writes the IRQ vectors. -func (f FileWriter) writeVectors(nmi, reset, irq string) error { +// writeVectorsTo writes the IRQ vectors to the specified writer. +func (f FileWriter) writeVectorsTo(w io.Writer, vectorsAddr uint16, nmi, reset, irq string) error { if f.options.CodeOnly { return nil } - addr := fmt.Sprintf("$%04X", f.app.VectorsStartAddress) + addr := fmt.Sprintf("$%04X", vectorsAddr) - _, err := fmt.Fprintf(f.mainWriter, "\n.pad %s\n", addr) + _, err := fmt.Fprintf(w, "\n.pad %s\n", addr) if err != nil { return fmt.Errorf("writing padding: %w", err) } - if err := f.writeSegment(addr); err != nil { - return err + if _, err := fmt.Fprintf(w, "\n.base %s\n\n", addr); err != nil { + return fmt.Errorf("writing segment: %w", err) } - if _, err := fmt.Fprintf(f.mainWriter, vectors, nmi, reset, irq); err != nil { + if _, err := fmt.Fprintf(w, vectors, nmi, reset, irq); err != nil { return fmt.Errorf("writing vectors: %w", err) } return nil } -// writeCode writes the code to the output. -func (f FileWriter) writeCode(bank *program.PRGBank) error { - endIndex := bank.LastNonZeroByte(f.options) - if err := f.writer.ProcessPRG(bank, endIndex); err != nil { - return fmt.Errorf("writing PRG: %w", err) - } - return nil +// bankFilename derives the bank output filename from the main output filename +// and the bank name. Matches the logic in pipeline.generateBankFilename. +func bankFilename(outputFilename, bankName string) string { + base := strings.TrimSuffix(outputFilename, ".asm") + suffix := strings.TrimPrefix(strings.ToLower(bankName), "prg_") + return base + "_" + suffix + ".asm" } diff --git a/internal/assembler/ca65/config.go b/internal/assembler/ca65/config.go index 4e19c8b..f61db95 100644 --- a/internal/assembler/ca65/config.go +++ b/internal/assembler/ca65/config.go @@ -46,7 +46,7 @@ func GenerateMapperConfig(conf Config) (string, error) { buf.WriteString(memoryConfigPart1) for _, bank := range conf.App.PRG { - if _, err := fmt.Fprintf(buf, memoryPrgBankTemplate, bank.Name+":", conf.App.CodeBaseAddress, len(bank.Offsets)); err != nil { + if _, err := fmt.Fprintf(buf, memoryPrgBankTemplate, bank.Name+":", bank.BaseAddress, len(bank.Offsets)); err != nil { return "", fmt.Errorf("writing memory bank line: %w", err) } } @@ -58,7 +58,7 @@ func GenerateMapperConfig(conf Config) (string, error) { buf.WriteString(segmentsConfigPart1) for _, bank := range conf.App.PRG { - if _, err := fmt.Fprintf(buf, segmentsPrgBankTemplate, bank.Name+":", bank.Name, conf.App.CodeBaseAddress); err != nil { + if _, err := fmt.Fprintf(buf, segmentsPrgBankTemplate, bank.Name+":", bank.Name, bank.BaseAddress); err != nil { return "", fmt.Errorf("writing segment bank line: %w", err) } } @@ -67,7 +67,7 @@ func GenerateMapperConfig(conf Config) (string, error) { // For multi-bank ROMs, vectors are included in each bank segment if len(conf.App.PRG) == 1 { lastBank := conf.App.PRG[0] - vectorStart := conf.App.CodeBaseAddress + uint16(len(lastBank.Offsets)) - 6 + vectorStart := lastBank.BaseAddress + uint16(len(lastBank.Offsets)) - 6 if _, err := fmt.Fprintf(buf, segmentsVectorsTemplate, lastBank.Name, vectorStart); err != nil { return "", fmt.Errorf("writing vectors segment: %w", err) } diff --git a/internal/assembler/ca65/file.go b/internal/assembler/ca65/file.go index c932cd0..b18b8d6 100644 --- a/internal/assembler/ca65/file.go +++ b/internal/assembler/ca65/file.go @@ -3,6 +3,7 @@ package ca65 import ( "fmt" "io" + "strings" "github.com/retroenv/retrodisasm/internal/assembler" "github.com/retroenv/retrodisasm/internal/options" @@ -40,6 +41,7 @@ type segmentWrite struct { type prgBankWrite struct { bank *program.PRGBank isMultiBank bool + firstBank bool } type customWrite func() error @@ -91,9 +93,9 @@ func (f FileWriter) Write() error { } isMultiBank := len(f.app.PRG) > 1 - for _, bank := range f.app.PRG { + for i, bank := range f.app.PRG { writes = append(writes, - prgBankWrite{bank: bank, isMultiBank: isMultiBank}, + prgBankWrite{bank: bank, isMultiBank: isMultiBank, firstBank: i == 0}, ) } @@ -153,51 +155,87 @@ func (f FileWriter) processWrite(write any) error { // writePRGBank writes a single PRG bank including constants, variables, code, and vectors. func (f FileWriter) writePRGBank(t prgBankWrite) error { - if err := f.writeConstants(t.bank); err != nil { - return err + endIndex := t.bank.LastNonZeroByte(f.options) + + // Skip empty banks that have no meaningful content. + // For multi-bank ROMs with headers/vectors, keep all banks for proper alignment. + if endIndex == 0 && (f.options.CodeOnly || !t.isMultiBank) { + return nil } - if err := f.writeVariables(t.bank); err != nil { - return err + + bankWriteCloser, err := f.newBankWriter(t.bank.Name) + if err != nil { + return fmt.Errorf("creating bank writer: %w", err) } - if err := f.writeCode(t.bank); err != nil { + defer func() { _ = bankWriteCloser.Close() }() + + if err := f.writeIncludeDirective(t); err != nil { return err } + + bankW := writer.New(f.app, bankWriteCloser, writer.Options{ + OffsetComments: f.options.OffsetComments, + }) + + if err := bankW.OutputAliasMap(t.bank.Constants); err != nil { + return fmt.Errorf("writing constants output alias map: %w", err) + } + if err := bankW.OutputAliasMap(t.bank.Variables); err != nil { + return fmt.Errorf("writing variables output alias map: %w", err) + } + + if !f.options.CodeOnly { + if err := f.writeSegmentTo(bankWriteCloser, t.bank.Name); err != nil { + return err + } + } + + if err := bankW.ProcessPRG(t.bank, endIndex); err != nil { + return fmt.Errorf("writing PRG: %w", err) + } + // For multi-bank ROMs, write vectors at end of each bank if t.isMultiBank && !f.options.CodeOnly { - if err := f.writeBankVectors(t.bank); err != nil { + if err := f.writeBankVectorsTo(bankWriteCloser, t.bank, endIndex); err != nil { return err } } return nil } -// writeSegment writes a segment header to the output. -func (f FileWriter) writeSegment(name string) error { - if name != "HEADER" { +// writeIncludeDirective writes a .include directive for the bank file when split-banks mode is enabled. +func (f FileWriter) writeIncludeDirective(t prgBankWrite) error { + if !f.options.SplitBanks { + return nil + } + if t.firstBank { if _, err := fmt.Fprintln(f.mainWriter); err != nil { - return fmt.Errorf("writing segment: %w", err) + return fmt.Errorf("writing newline: %w", err) } } - - _, err := fmt.Fprintf(f.mainWriter, ".segment \"%s\"\n\n", name) - if err != nil { - return fmt.Errorf("writing segment footer: %w", err) + bankFile := bankFilename(f.options.OutputFilename, t.bank.Name) + if _, err := fmt.Fprintf(f.mainWriter, ".include \"%s\"\n", bankFile); err != nil { + return fmt.Errorf("writing include directive: %w", err) } return nil } -// writeConstants writes constant aliases to the output. -func (f FileWriter) writeConstants(bank *program.PRGBank) error { - if err := f.writer.OutputAliasMap(bank.Constants); err != nil { - return fmt.Errorf("writing constants output alias map: %w", err) - } - return nil +// writeSegment writes a segment header to the main output. +func (f FileWriter) writeSegment(name string) error { + return f.writeSegmentTo(f.mainWriter, name) } -// writeVariables writes variable aliases to the output. -func (f FileWriter) writeVariables(bank *program.PRGBank) error { - if err := f.writer.OutputAliasMap(bank.Variables); err != nil { - return fmt.Errorf("writing variables output alias map: %w", err) +// writeSegmentTo writes a segment header to the specified writer. +func (f FileWriter) writeSegmentTo(w io.Writer, name string) error { + if name != "HEADER" { + if _, err := fmt.Fprintln(w); err != nil { + return fmt.Errorf("writing segment: %w", err) + } + } + + _, err := fmt.Fprintf(w, ".segment \"%s\"\n\n", name) + if err != nil { + return fmt.Errorf("writing segment footer: %w", err) } return nil } @@ -222,32 +260,38 @@ func (f FileWriter) writeCHR() error { return nil } -// writeCode writes the code to the output. -func (f FileWriter) writeCode(bank *program.PRGBank) error { - if !f.options.CodeOnly { - if err := f.writeSegment(bank.Name); err != nil { - return err - } +// writeBankVectorsTo writes vectors at the end of a bank for multi-bank ROMs. +// Each bank has its own NMI, Reset, and IRQ vectors stored in the last 6 bytes. +func (f FileWriter) writeBankVectorsTo(w io.Writer, bank *program.PRGBank, endIndex int) error { + // Multi-bank vectors must always sit in the last 6 bytes of the bank. + // Pad with explicit zeros so vectors are not emitted immediately after code. + vectorStartIndex := len(bank.Offsets) - 6 + padding := vectorStartIndex - endIndex + if padding < 0 { + return fmt.Errorf("bank data overlaps vectors: end_index=%d vector_start_index=%d", endIndex, vectorStartIndex) } - - endIndex := bank.LastNonZeroByte(f.options) - if err := f.writer.ProcessPRG(bank, endIndex); err != nil { - return fmt.Errorf("writing PRG: %w", err) + if padding > 0 { + if _, err := fmt.Fprintf(w, "\n.res %d, $00\n", padding); err != nil { + return fmt.Errorf("writing vector padding: %w", err) + } } - return nil -} -// writeBankVectors writes vectors at the end of a bank for multi-bank ROMs. -// Each bank has its own NMI, Reset, and IRQ vectors stored in the last 6 bytes. -func (f FileWriter) writeBankVectors(bank *program.PRGBank) error { // Vectors are: [0]=NMI, [1]=Reset, [2]=IRQ // Output as .addr directives using the addresses stored in the bank nmi := fmt.Sprintf("$%04X", bank.Vectors[0]) reset := fmt.Sprintf("$%04X", bank.Vectors[1]) irq := fmt.Sprintf("$%04X", bank.Vectors[2]) - if _, err := fmt.Fprintf(f.mainWriter, "\n.addr %s, %s, %s\n", nmi, reset, irq); err != nil { + if _, err := fmt.Fprintf(w, "\n.addr %s, %s, %s\n", nmi, reset, irq); err != nil { return fmt.Errorf("writing bank vectors: %w", err) } return nil } + +// bankFilename derives the bank output filename from the main output filename +// and the bank name. Matches the logic in pipeline.generateBankFilename. +func bankFilename(outputFilename, bankName string) string { + base := strings.TrimSuffix(outputFilename, ".asm") + suffix := strings.TrimPrefix(strings.ToLower(bankName), "prg_") + return base + "_" + suffix + ".asm" +} diff --git a/internal/assembler/nesasm/file.go b/internal/assembler/nesasm/file.go index 609e568..593ed6c 100644 --- a/internal/assembler/nesasm/file.go +++ b/internal/assembler/nesasm/file.go @@ -3,6 +3,7 @@ package nesasm import ( "fmt" "io" + "strings" "github.com/retroenv/retrodisasm/internal/assembler" "github.com/retroenv/retrodisasm/internal/options" @@ -26,6 +27,7 @@ type FileWriter struct { type prgBankWrite struct { bank *program.PRGBank isMultiBank bool + firstBank bool } type customWrite func() error @@ -62,9 +64,9 @@ func (f FileWriter) Write() error { nextBank := addPrgBankSelectors(int(f.app.CodeBaseAddress), f.app.PRG) isMultiBank := len(f.app.PRG) > 1 - for _, bank := range f.app.PRG { + for i, bank := range f.app.PRG { writes = append(writes, - prgBankWrite{bank: bank, isMultiBank: isMultiBank}, + prgBankWrite{bank: bank, isMultiBank: isMultiBank, firstBank: i == 0}, ) } @@ -98,18 +100,44 @@ func (f FileWriter) Write() error { // writePRGBank writes a single PRG bank including constants, variables, code, and vectors. func (f FileWriter) writePRGBank(t prgBankWrite) error { - if err := f.writeConstants(t.bank); err != nil { - return err + bankWriteCloser, err := f.newBankWriter(t.bank.Name) + if err != nil { + return fmt.Errorf("creating bank writer: %w", err) } - if err := f.writeVariables(t.bank); err != nil { - return err + defer func() { _ = bankWriteCloser.Close() }() + + if f.options.SplitBanks { + if t.firstBank { + if _, err := fmt.Fprintln(f.mainWriter); err != nil { + return fmt.Errorf("writing newline: %w", err) + } + } + bankFile := bankFilename(f.options.OutputFilename, t.bank.Name) + if _, err := fmt.Fprintf(f.mainWriter, " .include \"%s\"\n", bankFile); err != nil { + return fmt.Errorf("writing include directive: %w", err) + } + } + + bankW := writer.New(f.app, bankWriteCloser, writer.Options{ + DirectivePrefix: " ", + OffsetComments: f.options.OffsetComments, + }) + + if err := bankW.OutputAliasMap(t.bank.Constants); err != nil { + return fmt.Errorf("writing constants output alias map: %w", err) } - if err := f.writeCode(t.bank); err != nil { - return err + if err := bankW.OutputAliasMap(t.bank.Variables); err != nil { + return fmt.Errorf("writing variables output alias map: %w", err) } + + endIndex := t.bank.LastNonZeroByte(f.options) + if err := bankW.ProcessPRG(t.bank, endIndex); err != nil { + return fmt.Errorf("writing PRG: %w", err) + } + // For multi-bank ROMs, write vectors at end of each bank if t.isMultiBank && !f.options.CodeOnly { - if err := f.writeBankVectors(t.bank); err != nil { + if err := f.writeBankVectorsTo(bankWriteCloser, t.bank); err != nil { return err } } @@ -134,22 +162,6 @@ func (f FileWriter) writeROMHeader() error { return nil } -// writeConstants writes constant aliases to the output. -func (f FileWriter) writeConstants(bank *program.PRGBank) error { - if err := f.writer.OutputAliasMap(bank.Constants); err != nil { - return fmt.Errorf("writing constants output alias map: %w", err) - } - return nil -} - -// writeVariables writes variable aliases to the output. -func (f FileWriter) writeVariables(bank *program.PRGBank) error { - if err := f.writer.OutputAliasMap(bank.Variables); err != nil { - return fmt.Errorf("writing variables output alias map: %w", err) - } - return nil -} - // writeCHR writes the CHR content to the output. func (f FileWriter) writeCHR(nextBank int) func() error { return func() error { @@ -192,9 +204,10 @@ func (f FileWriter) writeVectors() error { return nil } -// writeBankVectors writes vectors at the end of a bank for multi-bank ROMs. -func (f FileWriter) writeBankVectors(bank *program.PRGBank) error { - if _, err := fmt.Fprintf(f.mainWriter, "\n .org $%04X\n", f.app.VectorsStartAddress); err != nil { +// writeBankVectorsTo writes vectors at the end of a bank for multi-bank ROMs. +func (f FileWriter) writeBankVectorsTo(w io.Writer, bank *program.PRGBank) error { + vectorsAddr := bank.BaseAddress + uint16(len(bank.Offsets)) - 6 + if _, err := fmt.Fprintf(w, "\n .org $%04X\n", vectorsAddr); err != nil { return fmt.Errorf("writing vector org: %w", err) } @@ -202,17 +215,16 @@ func (f FileWriter) writeBankVectors(bank *program.PRGBank) error { reset := fmt.Sprintf("$%04X", bank.Vectors[1]) irq := fmt.Sprintf("$%04X", bank.Vectors[2]) - if _, err := fmt.Fprintf(f.mainWriter, vectors, nmi, reset, irq); err != nil { + if _, err := fmt.Fprintf(w, vectors, nmi, reset, irq); err != nil { return fmt.Errorf("writing bank vectors: %w", err) } return nil } -// writeCode writes the code to the output. -func (f FileWriter) writeCode(bank *program.PRGBank) error { - endIndex := bank.LastNonZeroByte(f.options) - if err := f.writer.ProcessPRG(bank, endIndex); err != nil { - return fmt.Errorf("writing PRG: %w", err) - } - return nil +// bankFilename derives the bank output filename from the main output filename +// and the bank name. Matches the logic in pipeline.generateBankFilename. +func bankFilename(outputFilename, bankName string) string { + base := strings.TrimSuffix(outputFilename, ".asm") + suffix := strings.TrimPrefix(strings.ToLower(bankName), "prg_") + return base + "_" + suffix + ".asm" } diff --git a/internal/assembler/retroasm/nes.go b/internal/assembler/retroasm/nes.go index 8ca1999..e3766f6 100644 --- a/internal/assembler/retroasm/nes.go +++ b/internal/assembler/retroasm/nes.go @@ -2,8 +2,11 @@ package retroasm import ( "fmt" + "io" + "strings" "github.com/retroenv/retrodisasm/internal/program" + "github.com/retroenv/retrodisasm/internal/writer" "github.com/retroenv/retrogolib/arch/system/nes/cartridge" ) @@ -22,7 +25,7 @@ func (w *FileWriter) writeNES() error { for i, bank := range w.app.PRG { lastBank := i == len(w.app.PRG)-1 - if err := w.writeBank(bank, lastBank); err != nil { + if err := w.writeBank(bank, i == 0, lastBank); err != nil { return fmt.Errorf("writing bank %d: %w", i, err) } } @@ -85,34 +88,57 @@ func (w *FileWriter) writeHeader() error { } // writeBank writes a PRG bank. -func (w *FileWriter) writeBank(bank *program.PRGBank, lastBank bool) error { - if _, err := fmt.Fprintf(w.mainWriter, "\n.org $%04x\n\n", w.app.CodeBaseAddress); err != nil { +func (w *FileWriter) writeBank(bank *program.PRGBank, firstBank, lastBank bool) error { + bankWriteCloser, err := w.newBankWriter(bank.Name) + if err != nil { + return fmt.Errorf("creating bank writer: %w", err) + } + defer func() { _ = bankWriteCloser.Close() }() + + if w.options.SplitBanks { + if firstBank { + if _, err := fmt.Fprintln(w.mainWriter); err != nil { + return fmt.Errorf("writing newline: %w", err) + } + } + bankFile := bankFilename(w.options.OutputFilename, bank.Name) + if _, err := fmt.Fprintf(w.mainWriter, ".include \"%s\"\n", bankFile); err != nil { + return fmt.Errorf("writing include directive: %w", err) + } + } + + bankW := writer.New(w.app, bankWriteCloser, writer.Options{ + OffsetComments: w.options.OffsetComments, + }) + + if _, err := fmt.Fprintf(bankWriteCloser, "\n.org $%04x\n\n", bank.BaseAddress); err != nil { return fmt.Errorf("writing org directive: %w", err) } - if err := w.writer.OutputAliasMap(bank.Constants); err != nil { + if err := bankW.OutputAliasMap(bank.Constants); err != nil { return fmt.Errorf("writing constants: %w", err) } - if err := w.writer.OutputAliasMap(bank.Variables); err != nil { + if err := bankW.OutputAliasMap(bank.Variables); err != nil { return fmt.Errorf("writing variables: %w", err) } endIndex := bank.LastNonZeroByte(w.options) - if err := w.writer.ProcessPRG(bank, endIndex); err != nil { + if err := bankW.ProcessPRG(bank, endIndex); err != nil { return fmt.Errorf("writing PRG: %w", err) } if !w.options.CodeOnly { + vectorsAddr := bank.BaseAddress + uint16(len(bank.Offsets)) - 6 if lastBank { - if err := w.writeVectors(w.app.Handlers.NMI, w.app.Handlers.Reset, w.app.Handlers.IRQ); err != nil { + if err := writeVectorsTo(bankWriteCloser, vectorsAddr, w.app.Handlers.NMI, w.app.Handlers.Reset, w.app.Handlers.IRQ); err != nil { return fmt.Errorf("writing vectors: %w", err) } } else { nmi := fmt.Sprintf("$%04X", bank.Vectors[0]) reset := fmt.Sprintf("$%04X", bank.Vectors[1]) irq := fmt.Sprintf("$%04X", bank.Vectors[2]) - if err := w.writeVectors(nmi, reset, irq); err != nil { + if err := writeVectorsTo(bankWriteCloser, vectorsAddr, nmi, reset, irq); err != nil { return fmt.Errorf("writing vectors: %w", err) } } @@ -121,21 +147,29 @@ func (w *FileWriter) writeBank(bank *program.PRGBank, lastBank bool) error { return nil } -// writeVectors writes the IRQ vectors. -func (w *FileWriter) writeVectors(nmi, reset, irq string) error { - addr := fmt.Sprintf("$%04X", w.app.VectorsStartAddress) +// writeVectorsTo writes the IRQ vectors to the specified writer. +func writeVectorsTo(w io.Writer, vectorsAddr uint16, nmi, reset, irq string) error { + addr := fmt.Sprintf("$%04X", vectorsAddr) - if _, err := fmt.Fprintf(w.mainWriter, "\n.org %s\n\n", addr); err != nil { + if _, err := fmt.Fprintf(w, "\n.org %s\n\n", addr); err != nil { return fmt.Errorf("writing vector org: %w", err) } - if _, err := fmt.Fprintf(w.mainWriter, vectors, nmi, reset, irq); err != nil { + if _, err := fmt.Fprintf(w, vectors, nmi, reset, irq); err != nil { return fmt.Errorf("writing vectors: %w", err) } return nil } +// bankFilename derives the bank output filename from the main output filename +// and the bank name. Matches the logic in pipeline.generateBankFilename. +func bankFilename(outputFilename, bankName string) string { + base := strings.TrimSuffix(outputFilename, ".asm") + suffix := strings.TrimPrefix(strings.ToLower(bankName), "prg_") + return base + "_" + suffix + ".asm" +} + // writeCHR writes the CHR content. func (w *FileWriter) writeCHR() error { if len(w.app.CHR) == 0 { diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 12c0cc8..0c8f1f0 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -122,6 +122,7 @@ func createDisasmOptions(opts options.Program) options.Disassembler { disasmOptions.HexComments = !opts.NoHexComments disasmOptions.OffsetComments = !opts.NoOffsets disasmOptions.OutputUnofficialAsMnemonics = opts.OutputUnofficial + disasmOptions.SplitBanks = opts.SplitBanks disasmOptions.StopAtUnofficial = opts.StopAtUnofficial disasmOptions.ZeroBytes = opts.ZeroBytes diff --git a/internal/disasm/disasm_test.go b/internal/disasm/disasm_test.go index ed4a865..a695825 100644 --- a/internal/disasm/disasm_test.go +++ b/internal/disasm/disasm_test.go @@ -389,13 +389,13 @@ func TestDisasmDisambiguousInstructions(t *testing.T) { expected := `Reset: jmp _label_8005 - + _label_8003: .byte $04 ; branch into instruction detected: disambiguous instruction: nop z:$A9 - + _label_8004: .byte $a9 - + _label_8005: nop bmi _label_8003 @@ -554,7 +554,7 @@ func runDisasm(t *testing.T, setup func(options *options.Disassembler, cart *car writer := bufio.NewWriter(&buffer) newBankWriter := func(_ string) (io.WriteCloser, error) { - return nil, nil // nolint: nilnil + return nopWriteCloser{writer}, nil } app, err := disasm.Process(context.Background(), writer, newBankWriter) @@ -567,3 +567,9 @@ func runDisasm(t *testing.T, setup func(options *options.Disassembler, cart *car expected = trimStringList(expected) assert.Equal(t, expected, buf) } + +type nopWriteCloser struct { + io.Writer +} + +func (nopWriteCloser) Close() error { return nil } diff --git a/internal/options/options.go b/internal/options/options.go index 89971c4..3277336 100644 --- a/internal/options/options.go +++ b/internal/options/options.go @@ -41,6 +41,7 @@ type OutputFlags struct { NoHexComments bool `flag:"nohexcomments" usage:"omit hex opcode bytes in comments"` NoOffsets bool `flag:"nooffsets" usage:"omit file offsets in comments"` OutputUnofficial bool `flag:"output-unofficial" usage:"use mnemonics for unofficial opcodes (incompatible with -verify)"` + SplitBanks bool `flag:"split-banks" usage:"write each PRG bank as a separate .asm file"` StopAtUnofficial bool `flag:"stop-at-unofficial" usage:"stop tracing at unofficial opcodes unless branched to"` ZeroBytes bool `flag:"z" usage:"include trailing zero bytes in banks"` } @@ -65,8 +66,10 @@ type Disassembler struct { HexComments bool OffsetComments bool OutputUnofficialAsMnemonics bool // output unofficial opcodes as mnemonics instead of .byte + SplitBanks bool // write each PRG bank as a separate .asm file StopAtUnofficial bool // stop tracing at unofficial opcodes unless explicitly branched to ZeroBytes bool + OutputFilename string // output filename for bank file generation } // NewDisassembler returns a new options instance with default options. diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go index 414a728..f0583da 100644 --- a/internal/pipeline/pipeline.go +++ b/internal/pipeline/pipeline.go @@ -5,6 +5,8 @@ import ( "context" "fmt" "io" + "os" + "path/filepath" "strings" "github.com/retroenv/retrodisasm/internal/arch/chip8" @@ -74,6 +76,7 @@ func (p *Pipeline) ExecuteWithCartridge(ctx context.Context, cart *cartridge.Car // Update disasm options with system disasmOpts.System = system disasmOpts.Binary = opts.Binary + disasmOpts.OutputFilename = filepath.Base(opts.Output) // When using binary mode, only output code without NES-specific segments if opts.Binary { @@ -95,7 +98,7 @@ func (p *Pipeline) ExecuteWithCartridge(ctx context.Context, cart *cartridge.Car p.printInfo(opts, cart, system) // Run disassembly - result, err := p.runDisassembly(ctx, dis, writer) + result, err := p.runDisassembly(ctx, dis, writer, disasmOpts, opts.Output) if err != nil { return nil, fmt.Errorf("disassembling: %w", err) } @@ -182,9 +185,15 @@ func (p *Pipeline) createDisassemblerForSystem(system arch.System, paramConverte } // runDisassembly executes the disassembly process. -func (p *Pipeline) runDisassembly(ctx context.Context, dis *disasm.Disasm, writer io.Writer) (*program.Program, error) { +func (p *Pipeline) runDisassembly(ctx context.Context, dis *disasm.Disasm, writer io.Writer, + opts options.Disassembler, outputPath string) (*program.Program, error) { + newBankWriter := func(bankName string) (io.WriteCloser, error) { - return &nopCloser{writer}, nil + if !opts.SplitBanks { + return &nopCloser{writer}, nil + } + bankFile := generateBankFilename(outputPath, bankName) + return os.Create(bankFile) } result, err := dis.Process(ctx, writer, newBankWriter) @@ -194,6 +203,18 @@ func (p *Pipeline) runDisassembly(ctx context.Context, dis *disasm.Disasm, write return result, nil } +// generateBankFilename creates a per-bank output filename from the main output path +// and the bank name. For example, ("output.asm", "PRG_BANK_3") -> "output_bank_3.asm". +func generateBankFilename(outputPath, bankName string) string { + ext := filepath.Ext(outputPath) + base := outputPath[:len(outputPath)-len(ext)] + + // Convert bank name to lowercase filename suffix: "PRG_BANK_3" -> "bank_3" + suffix := strings.TrimPrefix(strings.ToLower(bankName), "prg_") + + return base + "_" + suffix + ext +} + // printInfo prints information about the ROM being processed. func (p *Pipeline) printInfo(opts options.Program, cart *cartridge.Cartridge, system arch.System) { if opts.Quiet { diff --git a/internal/program/prg.go b/internal/program/prg.go index 1db7f47..1270911 100644 --- a/internal/program/prg.go +++ b/internal/program/prg.go @@ -15,7 +15,8 @@ func NewPRGBank(size int) *PRGBank { // PRGBank defines a PRG bank. type PRGBank struct { - Name string + Name string + BaseAddress uint16 Offsets []Offset Vectors [3]uint16 diff --git a/internal/writer/writer.go b/internal/writer/writer.go index ec35629..23f1708 100644 --- a/internal/writer/writer.go +++ b/internal/writer/writer.go @@ -26,6 +26,8 @@ type Writer struct { app *program.Program options Options writer io.Writer + + emittedAliases map[string]uint16 } // Options of the writer. @@ -40,6 +42,8 @@ func New(app *program.Program, writer io.Writer, options Options) *Writer { app: app, options: options, writer: writer, + + emittedAliases: map[string]uint16{}, } } @@ -119,22 +123,29 @@ func (w Writer) OutputAliasMap(aliases map[string]uint16) error { return nil } - if _, err := fmt.Fprintln(w.writer); err != nil { - return fmt.Errorf("writing line: %w", err) + // sort the aliases by name before outputting to avoid random map order + toEmit := make([]string, 0, len(aliases)) + for name, address := range aliases { + if previousAddress, ok := w.emittedAliases[name]; ok && previousAddress == address { + continue + } + toEmit = append(toEmit, name) } + if len(toEmit) == 0 { + return nil + } + slices.Sort(toEmit) - // sort the aliases by name before outputting to avoid random map order - names := make([]string, 0, len(aliases)) - for constant := range aliases { - names = append(names, constant) + if _, err := fmt.Fprintln(w.writer); err != nil { + return fmt.Errorf("writing line: %w", err) } - slices.Sort(names) - for _, constant := range names { - address := aliases[constant] - if _, err := fmt.Fprintf(w.writer, "%s = $%04X\n", constant, address); err != nil { + for _, name := range toEmit { + address := aliases[name] + if _, err := fmt.Fprintf(w.writer, "%s = $%04X\n", name, address); err != nil { return fmt.Errorf("writing alias: %w", err) } + w.emittedAliases[name] = address } if _, err := fmt.Fprintln(w.writer); err != nil { diff --git a/internal/writer/writer_test.go b/internal/writer/writer_test.go new file mode 100644 index 0000000..b87fd6b --- /dev/null +++ b/internal/writer/writer_test.go @@ -0,0 +1,41 @@ +package writer + +import ( + "bytes" + "strings" + "testing" + + "github.com/retroenv/retrodisasm/internal/program" + "github.com/retroenv/retrogolib/assert" +) + +func TestOutputAliasMap_SkipsDuplicateReEmission(t *testing.T) { + app := &program.Program{} + var buf bytes.Buffer + w := New(app, &buf, Options{}) + + aliases := map[string]uint16{ + "PPU_CTRL": 0x2000, + "PPU_MASK": 0x2001, + } + + assert.NoError(t, w.OutputAliasMap(aliases)) + assert.NoError(t, w.OutputAliasMap(aliases)) + + out := buf.String() + assert.Equal(t, 1, strings.Count(out, "PPU_CTRL = $2000")) + assert.Equal(t, 1, strings.Count(out, "PPU_MASK = $2001")) +} + +func TestOutputAliasMap_EmitsWhenAddressDiffers(t *testing.T) { + app := &program.Program{} + var buf bytes.Buffer + w := New(app, &buf, Options{}) + + assert.NoError(t, w.OutputAliasMap(map[string]uint16{"FOO": 0x0010})) + assert.NoError(t, w.OutputAliasMap(map[string]uint16{"FOO": 0x0020})) + + out := buf.String() + assert.Equal(t, 1, strings.Count(out, "FOO = $0010")) + assert.Equal(t, 1, strings.Count(out, "FOO = $0020")) +}