Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions Sources/Bubilator88Core/FrameCompositor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ package final class FrameCompositor {
textRows: frame.crtcLines,
reverseDisplay: crtc.reverseDisplay,
crtcStopped: !crtc.displayEnabled,
monoText: !bus.colorMode
monoText: !bus.colorMode,
blockCursor: frame.cursorVisible && (crtc.cursorMode & 0x02) != 0
? (x: crtc.cursorX, y: crtc.cursorY) : nil
)
// B/W graphics: palette[0] above is the background; lit dots take the
// attribute color, so color 0 gets its own entry.
Expand Down Expand Up @@ -196,15 +198,32 @@ package final class FrameCompositor {
/// color and reverse bits when the CRTC stops (`CRTC::ClearText`), and ORs
/// in 7 when port 0x30 bit 1 selects mono text — so The Man I Love, which
/// stops the CRTC with mono text, keeps its B/W title white.
///
/// `blockCursor` is the visible block cursor's cell. The CRTC applies the
/// cursor as a reverse attribute, and B/W graphics take their reverse from
/// the attributes, so the cursor inverts the graphics cell too (BubiC XORs
/// it into the shared `attrib.expand`). N88-日本語BASIC draws its text into
/// graphics over blank text VRAM, so this is the only place its cursor shows.
package static func attributeGraphAttributes(
from attrData: [UInt8],
textDisplayMode: Pc88Bus.TextDisplayMode,
textRows: Int,
reverseDisplay: Bool,
crtcStopped: Bool = false,
monoText: Bool = false
monoText: Bool = false,
blockCursor: (x: Int, y: Int)? = nil
) -> [UInt8] {
guard textDisplayMode == .disabled else { return attrData }
guard textDisplayMode == .disabled else {
guard let blockCursor,
(0..<ScreenRenderer.textCols80).contains(blockCursor.x),
(0..<textRows).contains(blockCursor.y)
else { return attrData }
let index = blockCursor.y * ScreenRenderer.textCols80 + blockCursor.x
guard index < attrData.count else { return attrData }
var attrs = attrData
attrs[index] ^= 0x01
return attrs
}
let defaultAttr: UInt8 = crtcStopped
? (monoText ? 0xE0 : 0x00)
: 0xE0 | (reverseDisplay ? 0x01 : 0x00)
Expand Down
3 changes: 2 additions & 1 deletion Sources/Bubilator88Core/PC88.swift
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,8 @@ public final class PC88: @unchecked Sendable {
}

/// The 8×8 font ROM glyph for a character code, one byte per row, MSB on
/// the left. All zero when no font ROM is loaded.
/// the left. Without a FONT.ROM it comes from the built-in font, which
/// draws 0x00-0xF7 and leaves 0xF8-0xFF blank as the real ROM does.
public func glyph(for code: UInt8) -> [UInt8] {
machine.fontROM.glyph(for: code)
}
Expand Down
39 changes: 39 additions & 0 deletions Tests/Bubilator88CoreTests/FrameCompositorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,45 @@ struct FrameCompositorTests {
#expect(result == attrData)
}

/// N88-日本語BASIC draws its text into B/W graphics and leaves text VRAM
/// blank, so its block cursor shows only through the inverted graphics cell.
@Test("attribute graphics invert the block cursor cell")
func attributeGraphAttributesInvertBlockCursorCell() {
var attrData = Array(repeating: UInt8(0xE0), count: 80 * 20)
attrData[5 * 80 + 1] = 0xE1

let result = FrameCompositor.attributeGraphAttributes(
from: attrData,
textDisplayMode: .enabled,
textRows: 20,
reverseDisplay: false,
blockCursor: (x: 0, y: 5)
)
#expect(result[5 * 80] == 0xE1)
#expect(result[5 * 80 + 1] == 0xE1)
#expect(result[0] == 0xE0)

// A reverse cell under the cursor turns back to normal.
let onReverse = FrameCompositor.attributeGraphAttributes(
from: attrData,
textDisplayMode: .enabled,
textRows: 20,
reverseDisplay: false,
blockCursor: (x: 1, y: 5)
)
#expect(onReverse[5 * 80 + 1] == 0xE0)

// Off-screen positions are ignored.
let offScreen = FrameCompositor.attributeGraphAttributes(
from: attrData,
textDisplayMode: .enabled,
textRows: 20,
reverseDisplay: false,
blockCursor: (x: 0, y: 20)
)
#expect(offScreen == attrData)
}

@Test("attribute graphics keep reverse default when display is disabled")
func attributeGraphAttributesCarryReverseDisplayIntoNeutralState() {
let result = FrameCompositor.attributeGraphAttributes(
Expand Down
Loading