Tachibana
Language: English

Group 12 — Blitter Official

12,1 Blitter Busy · 12,2 Simple Blit Copy · 12,3 Complex Blit Copy · 12,4 Blit Image

The blitter copies memory blocks on the RP2040 side, without going through the 65C02. An address there has 24 bits: a page and a 16-bit address. Pages: $00 the 6502 memory, $80 and $81 the video memory (in mode 0, one byte per pixel, 320 per line: pixel (x, y) is at y × 320 + x, past 65535 in page $81), $90 the graphics memory, $A0 and up the memory banks in flash (read only).

The examples copy to the cleared screen, then read the pixels back with 5,33.

12,1 Blitter Busy Official

Tells whether the blitter is busy copying.

Inputs: none.

Outputs
BytesTypeMeaning
P0uint8non zero if a copy is running; always 0 today

The firmware completes each copy before returning: the call always returns 0, before as well as right after a copy. It stays useful for a program that must also run on a firmware where the copy would be done in the background. NeoBASIC has no keyword for this call.

Firmware documentation text

Returns a non zero value in Parameter:0 if the blitter/DMA system is currently transferring data, used to check availability and transfer completion.

C (llvm-mos)
/* Savoir si le blitter est occupé (groupe 12, fonction 1). */
#include <stdio.h>
#include <neo6502.h>
#include <neo/api.h>

/* Appel brut de la boîte aux lettres : fonction puis groupe (écrit en dernier, il lance l'appel). */
static void api(uint8_t groupe, uint8_t fonction) {
    ControlPort.function = fonction;
    ControlPort.command = groupe;
    while (ControlPort.command)        /* attendre la fin */
        ;
}
static void libre(void) {              /* attendre que la boîte aux lettres soit libre */
    while (ControlPort.command)
        ;
}

/* 12,2 : copie de n octets de page:adresse vers page:adresse ; rend l'indicateur d'erreur. */
static uint8_t copier(uint8_t ps, uint16_t as, uint8_t pd, uint16_t ad, uint16_t n) {
    libre();
    ControlPort.params[0] = ps;                 /* P0 : page source ($00 RAM du 6502, $80-$81 vidéo, $90 graphique) */
    ControlPort.params[1] = as & 0xFF;          /* P1-P2 : adresse source */
    ControlPort.params[2] = as >> 8;
    ControlPort.params[3] = pd;                 /* P3 : page destination */
    ControlPort.params[4] = ad & 0xFF;          /* P4-P5 : adresse destination */
    ControlPort.params[5] = ad >> 8;
    ControlPort.params[6] = n & 0xFF;           /* P6-P7 : nombre d'octets */
    ControlPort.params[7] = n >> 8;
    api(12, 2);
    return ControlPort.error;
}

static uint8_t occupe(void) {                   /* Blitter Busy */
    libre();
    api(12, 1);
    return ControlPort.params[0];
}

static char src[] = "ABCD", dst[5];

int main(void) {
    uint8_t avant = occupe();
    copier(0x00, (uint16_t)src, 0x00, (uint16_t)dst, 4);
    uint8_t apres = occupe();                   /* la copie est déjà finie */
    printf("Occupe avant : %u, apres une copie : %u\n", avant, apres);
    return 0;
}
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
Occupe avant : 0, apres une copie : 0
Mad Pascal
// Savoir si le blitter est occupé (groupe 12, fonction 1).
program blit1;
uses crt, neo6502;
var src, dst: array[0..3] of byte;
    avant, apres: boolean;
begin
  avant := NeoBlitterBusy;                      // Blitter Busy
  NeoBlitterCopy(word(@src), word(@dst), 4);    // 12,2 : page $00 (RAM du 6502) des deux côtés
  apres := NeoBlitterBusy;                      // la copie est déjà finie
  writeln('Occupe avant : ', ord(avant), ', apres une copie : ', ord(apres));
end.
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
Occupe avant : 0, apres une copie : 0
NeoBASIC
10 ' Savoir si le blitter est occupe (groupe 12, fonction 1)
20 gosub 900 : avant = o
30 s = alloc(4) : d = alloc(4) : ps = 0 : as = s : pd = 0 : ad = d : n = 4 : gosub 1000
40 gosub 900 : apres = o : ' la copie est deja finie
50 print "Occupe avant : ";avant;", apres une copie : ";apres
60 end
900 ' Blitter Busy : o
910 while peek($ff00) : wend : poke $ff01, 1 : poke $ff00, 12
920 while peek($ff00) : wend : o = peek($ff04)
930 return
1000 ' 12,2 : copie de n octets de ps:as vers pd:ad ; erreur dans e
1010 while peek($ff00) : wend
1020 poke $ff04, ps : doke $ff05, as : poke $ff07, pd : doke $ff08, ad : doke $ff0a, n
1030 poke $ff01, 2 : poke $ff00, 12
1040 while peek($ff00) : wend : e = peek($ff02)
1050 return
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
Occupe avant : 0, apres une copie : 0
65C02 assembly (64tass)
; Savoir si le blitter est occupé (groupe 12, fonction 1).
        .cpu "65c02"
        * = $0800
ControlPort    = $FF00             ; +0 groupe, +1 fonction, +2 erreur, +4.. paramètres
WaitMessage    = $FFF4             ; attend que la boîte aux lettres soit libre
WriteCharacter = $FFF1             ; affiche le caractère de A

start: lda #<occupe
        ldx #>occupe
        jsr envoi
        lda ControlPort+4
        sta avant
        lda #<copie
        ldx #>copie
        jsr envoi
        lda #<occupe
        ldx #>occupe
        jsr envoi
        lda ControlPort+4          ; la copie est déjà finie
        sta apres
        lda #<t0
        ldx #>t0
        jsr ecrire
        lda avant
        jsr decimal
        lda #<t1
        ldx #>t1
        jsr ecrire
        lda apres
        jsr decimal
        lda #13
        jsr WriteCharacter
        rts

occupe: .byte 12, 1, 0             ; 12,1 : Blitter Busy
copie:  .byte 12, 2, 8, 0          ; 12,2 : de la page $00 (RAM du 6502)...
        .word src
        .byte 0          ; ... vers la page $00
        .word dst, 4
src:    .text "ABCD"
dst:    .fill 4
t0:     .text "Occupe avant : ", 0
t1:     .text ", apres une copie : ", 0
avant:  .byte 0
apres:  .byte 0

; Appel décrit par un bloc dont l'adresse est dans A (poids faible) et X (poids fort) :
; groupe, fonction, nombre de paramètres, puis les paramètres (P0, P1...).
envoi:  sta blk
        stx blk+1
        jsr WaitMessage
        ldy #2
        lda (blk),y                ; nombre de paramètres
        clc
        adc #3
        sta fin
        ldy #3
_copie: cpy fin
        beq _lance
        lda (blk),y
        sta ControlPort+1,y        ; octet 3 du bloc -> P0 ($FF04)...
        iny
        bra _copie
_lance: ldy #1
        lda (blk),y
        tax
        lda (blk)
        jmp appel

; Affiche la chaîne terminée par 0 dont l'adresse est dans A (poids faible) et X (poids fort).
ecrire: sta ptr
        stx ptr+1
        ldy #0
_e:     lda (ptr),y
        beq _f
        jsr WriteCharacter
        iny
        bne _e
_f:     rts

; Affiche A (0-255) en décimal, sans zéros de tête.
decimal:
        phx
        phy
        ldx #0
        ldy #100
        jsr _chiffre
        ldy #10
        jsr _chiffre
        ora #'0'
        jsr WriteCharacter
        ply
        plx
        rts
_chiffre:
        sty diviseur
        ldy #0
_div:   cmp diviseur
        bcc _fini
        sbc diviseur
        iny
        bne _div
_fini:  pha
        tya
        bne _ecrit
        cpx #0
        beq _saute
_ecrit: ora #'0'
        jsr WriteCharacter
        ldx #1
_saute: pla
        rts

; Appel : A = groupe, X = fonction (les paramètres sont déjà en place).
appel:  stx ControlPort+1
        sta ControlPort            ; écrit en dernier : lance l'appel
        jmp WaitMessage

ptr      = $80
blk      = $82
diviseur: .byte 0
fin:     .byte 0
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
Occupe avant : 0, apres une copie : 0

12,2 Simple Blit Copy Official

Copies a run of bytes from one memory page to another (6502, video, graphics memory).

Inputs
BytesTypeMeaning
P0uint8source page
P1-P2addrsource address within the page
P3uint8destination page
P4-P5addrdestination address within the page
P6-P7uint16number of bytes
Outputs
BytesTypeMeaning
erreuruint81 if the start or end of either area does not exist, or if the destination is a bank

The areas may overlap. An area does not run from one page into the next: in video memory, 65536 bytes in page $80, the rest in page $81. The example copies a text within 6502 memory, eight pixels of colour 5 to the screen (line 100, x 100 to 107; the next pixel stays at 0), then shows a missing page being refused. NeoBASIC has no keyword for this call.

Firmware documentation text

Copy Parameters:6,7 bytes of internal memory from Parameter:0:Parameters:1,2 to Parameter:3:Parameters:4,5. Sets error flag if the transfer is not possible (e.g. illegal write addresses). The upper 8 bits of the address are : 6502 RAM (00) VideoRAM (80,81) Graphics RAM(90)

C (llvm-mos)
/* Copier des octets en mémoire et vers l'écran (groupe 12, fonction 2). */
#include <stdio.h>
#include <neo6502.h>
#include <neo/api.h>

/* Appel brut de la boîte aux lettres : fonction puis groupe (écrit en dernier, il lance l'appel). */
static void api(uint8_t groupe, uint8_t fonction) {
    ControlPort.function = fonction;
    ControlPort.command = groupe;
    while (ControlPort.command)        /* attendre la fin */
        ;
}
static void libre(void) {              /* attendre que la boîte aux lettres soit libre */
    while (ControlPort.command)
        ;
}

/* 12,2 : copie de n octets de page:adresse vers page:adresse ; rend l'indicateur d'erreur. */
static uint8_t copier(uint8_t ps, uint16_t as, uint8_t pd, uint16_t ad, uint16_t n) {
    libre();
    ControlPort.params[0] = ps;                 /* P0 : page source ($00 RAM du 6502, $80-$81 vidéo, $90 graphique) */
    ControlPort.params[1] = as & 0xFF;          /* P1-P2 : adresse source */
    ControlPort.params[2] = as >> 8;
    ControlPort.params[3] = pd;                 /* P3 : page destination */
    ControlPort.params[4] = ad & 0xFF;          /* P4-P5 : adresse destination */
    ControlPort.params[5] = ad >> 8;
    ControlPort.params[6] = n & 0xFF;           /* P6-P7 : nombre d'octets */
    ControlPort.params[7] = n >> 8;
    api(12, 2);
    return ControlPort.error;
}

static char texte[] = "ABCDEFGH", copie[9];
static uint8_t couleur[8] = {5, 5, 5, 5, 5, 5, 5, 5};

int main(void) {
    putchar(12);                                /* écran effacé : on relit des pixels */
    copier(0x00, (uint16_t)texte, 0x00, (uint16_t)copie, 8);        /* RAM du 6502 → RAM du 6502 */
    copier(0x00, (uint16_t)couleur, 0x80, 100 * 320 + 100, 8);      /* → écran, ligne 100, x 100 à 107 */
    uint8_t a = neo_graphics_read_pixel(100, 100), b = neo_graphics_read_pixel(107, 100);
    uint8_t c = neo_graphics_read_pixel(108, 100);
    uint8_t e = copier(0x70, 0, 0x00, (uint16_t)copie, 8);           /* page $70 : n'existe pas */
    printf("En memoire : %s\n", copie);
    printf("A l'ecran : %u %u, apres : %u\n", a, b, c);
    printf("Page $70 : erreur %u\n", e);
    return 0;
}
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
En memoire : ABCDEFGH
A l'ecran : 5 5, apres : 0
Page $70 : erreur 1
Mad Pascal
// Copier des octets en mémoire et vers l'écran (groupe 12, fonction 2).
program blit2;
uses crt, neo6502;
var texte, copie: string[8];
    couleur: array[0..7] of byte;
    a, b, c, e: byte;
begin
  write(chr(12));                               // écran effacé : on relit des pixels
  texte := 'ABCDEFGH';
  fillchar(@couleur, 8, 5);
  copie := '';
  NeoBlitterCopy(word(@texte) + 1, word(@copie) + 1, 8);   // Simple Blit Copy : RAM du 6502 → RAM du 6502
  copie[0] := chr(8);
  NeoBlitterCopy(word(@couleur), $800000 + 100 * 320 + 100, 8);   // → écran (page $80), ligne 100, x 100 à 107
  a := NeoGetPixel(100, 100);
  b := NeoGetPixel(107, 100);
  c := NeoGetPixel(108, 100);
  NeoBlitterCopy($700000, word(@copie) + 1, 8); // page $70 : n'existe pas
  NeoWaitMessage;
  e := NeoMessage.error;
  writeln('En memoire : ', copie);
  writeln('A l''ecran : ', a, ' ', b, ', apres : ', c);
  writeln('Page $70 : erreur ', e);
end.
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
En memoire : ABCDEFGH
A l'ecran : 5 5, apres : 0
Page $70 : erreur 1
NeoBASIC
10 ' Copier des octets en memoire et vers l'ecran (groupe 12, fonction 2)
20 cls : ' ecran efface : on relit des pixels
30 t = alloc(8) : c = alloc(9) : k = alloc(8)
40 for i = 0 to 7 : poke t + i, 65 + i : poke k + i, 5 : next i : ' ABCDEFGH ; 8 octets de couleur 5
50 ps = 0 : as = t : pd = 0 : ad = c : n = 8 : gosub 1000 : ' Simple Blit Copy : RAM du 6502 vers RAM du 6502
60 ps = 0 : as = k : pd = $80 : ad = 32100 : gosub 1000 : ' vers l'ecran (page $80) : 100 * 320 + 100, ligne 100, x 100 a 107
70 a = point(100,100) : b = point(107,100) : d = point(108,100)
80 ps = $70 : as = 0 : pd = 0 : ad = c : gosub 1000 : ' page $70 : n'existe pas
90 s$ = "" : for i = 0 to 7 : s$ = s$ + chr$(peek(c + i)) : next i
100 print "En memoire : ";s$
110 print "A l'ecran : ";a;" ";b;", apres : ";d
120 print "Page $70 : erreur ";e
130 end
1000 ' 12,2 : copie de n octets de ps:as vers pd:ad ; erreur dans e
1010 while peek($ff00) : wend
1020 poke $ff04, ps : doke $ff05, as : poke $ff07, pd : doke $ff08, ad : doke $ff0a, n
1030 poke $ff01, 2 : poke $ff00, 12
1040 while peek($ff00) : wend : e = peek($ff02)
1050 return
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
En memoire : ABCDEFGH
A l'ecran : 5 5, apres : 0
Page $70 : erreur 1
65C02 assembly (64tass)
; Copier des octets en mémoire et vers l'écran (groupe 12, fonction 2).
        .cpu "65c02"
        * = $0800
ControlPort    = $FF00             ; +0 groupe, +1 fonction, +2 erreur, +4.. paramètres
WaitMessage    = $FFF4             ; attend que la boîte aux lettres soit libre
WriteCharacter = $FFF1             ; affiche le caractère de A

start:   lda #12                    ; écran effacé : on relit des pixels
        jsr WriteCharacter
        lda #<c1
        ldx #>c1
        jsr envoi
        lda #<c2
        ldx #>c2
        jsr envoi
        lda #100
        ldy #100
        jsr pixel
        sta a
        lda #107
        ldy #100
        jsr pixel
        sta b
        lda #108
        ldy #100
        jsr pixel
        sta c
        lda #<c3
        ldx #>c3
        jsr envoi
        lda ControlPort+2
        sta e
        lda #<t0
        ldx #>t0
        jsr ecrire
        lda #<copie
        ldx #>copie
        jsr ecrire
        lda #13
        jsr WriteCharacter
        lda #<t1
        ldx #>t1
        jsr ecrire
        lda a
        jsr decimal
        lda #' '
        jsr WriteCharacter
        lda b
        jsr decimal
        lda #<t2
        ldx #>t2
        jsr ecrire
        lda c
        jsr decimal
        lda #13
        jsr WriteCharacter
        lda #<t3
        ldx #>t3
        jsr ecrire
        lda e
        jsr decimal
        lda #13
        jsr WriteCharacter
        rts

; Couleur du fond (5,33) en (A, Y), X et Y inférieurs à 256, rendue dans A.
pixel:  pha
        jsr WaitMessage
        pla
        sta ControlPort+4          ; P0-P1 : X
        stz ControlPort+5
        sty ControlPort+6          ; P2-P3 : Y
        stz ControlPort+7
        lda #5
        ldx #33                    ; 5,33 : Read Pixel
        jsr appel
        lda ControlPort+4
        rts

c1:     .byte 12, 2, 8, 0          ; 12,2 : RAM du 6502 → RAM du 6502
        .word texte
        .byte 0
        .word copie, 8
c2:     .byte 12, 2, 8, 0          ; → écran : page $80, ligne 100, x 100 à 107
        .word couleur
        .byte $80
        .word 100*320+100, 8
c3:     .byte 12, 2, 8, $70        ; page $70 : n'existe pas
        .word 0
        .byte 0
        .word copie, 8
texte:  .text "ABCDEFGH"
copie:  .fill 9                    ; zéro final : chaîne terminée
couleur: .byte 5, 5, 5, 5, 5, 5, 5, 5
t0:     .text "En memoire : ", 0
t1:     .text "A l'ecran : ", 0
t2:     .text ", apres : ", 0
t3:     .text "Page $70 : erreur ", 0
a:      .byte 0
b:      .byte 0
c:      .byte 0
e:      .byte 0

; Appel décrit par un bloc dont l'adresse est dans A (poids faible) et X (poids fort) :
; groupe, fonction, nombre de paramètres, puis les paramètres (P0, P1...).
envoi:  sta blk
        stx blk+1
        jsr WaitMessage
        ldy #2
        lda (blk),y                ; nombre de paramètres
        clc
        adc #3
        sta fin
        ldy #3
_copie: cpy fin
        beq _lance
        lda (blk),y
        sta ControlPort+1,y        ; octet 3 du bloc -> P0 ($FF04)...
        iny
        bra _copie
_lance: ldy #1
        lda (blk),y
        tax
        lda (blk)
        jmp appel

; Affiche la chaîne terminée par 0 dont l'adresse est dans A (poids faible) et X (poids fort).
ecrire: sta ptr
        stx ptr+1
        ldy #0
_e:     lda (ptr),y
        beq _f
        jsr WriteCharacter
        iny
        bne _e
_f:     rts

; Affiche A (0-255) en décimal, sans zéros de tête.
decimal:
        phx
        phy
        ldx #0
        ldy #100
        jsr _chiffre
        ldy #10
        jsr _chiffre
        ora #'0'
        jsr WriteCharacter
        ply
        plx
        rts
_chiffre:
        sty diviseur
        ldy #0
_div:   cmp diviseur
        bcc _fini
        sbc diviseur
        iny
        bne _div
_fini:  pha
        tya
        bne _ecrit
        cpx #0
        beq _saute
_ecrit: ora #'0'
        jsr WriteCharacter
        ldx #1
_saute: pla
        rts

; Appel : A = groupe, X = fonction (les paramètres sont déjà en place).
appel:  stx ControlPort+1
        sta ControlPort            ; écrit en dernier : lance l'appel
        jmp WaitMessage

ptr      = $80
blk      = $82
diviseur: .byte 0
fin:     .byte 0
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
En memoire : ABCDEFGH
A l'ecran : 5 5, apres : 0
Page $70 : erreur 1

12,3 Complex Blit Copy Changed by Trinity

Copies a rectangle to another one, converting the pixel format on the way, with optional transparency.

Inputs
BytesTypeMeaning
P0uint8action: 0 copy, 1 copy except the transparent value, 2 solid colour where the source is not transparent
P1-P2addraddress of the source area descriptor (12 bytes)
P3-P4addraddress of the target area descriptor (12 bytes)
Outputs
BytesTypeMeaning
erreuruint8non zero if the copy is impossible (format, address, target in a bank)

Area descriptor, in 6502 memory: bytes 0-1 address and 2 page; 3 is 0; 4-5 stride (added to the address from one line to the next; 0 repeats the same line, negative goes upward); 6 format (0 bytes; 1 pairs of 4 bits and 2 single bits, source only; 3 and 4 high or low half of the byte, target only; since Trinity 0.16.69, 5 and 6 two pixels per byte as target, the first pixel in the high (even x) or low (odd x) half, for the 16-colour pages of mode 2: according to the documentation, not tried on the bench); then, source only, 7 transparent value, 8 solid colour, 9 height, 10-11 width in pixels. The copy works on source bytes: the width is rounded down to a whole byte. The example unfolds 4 bytes into 8 pixels (1 to 8) on two lines, thanks to a zero source stride. NeoBASIC has no keyword for this call.

Firmware documentation text

Copy a source rectangular area to a destination rectangular area. It's oriented toward copying graphics data, but can be used as a more general-purpose memory mover. The source and target areas may be different formats, and the copy will convert the data on the fly. For example, you can expand 4bpp source graphics (two pixels per byte) into the 1 pixel per byte framebuffer. However, the blitting is byte-oriented. So the source width is always rounded down to the nearest full byte.

Parameter (0) is the blit action:

0 = copy 1 = copymasked - copy, but only where src is not the transparent value. 2 = solidmasked - set target to constant solid value, but only where src is not the transparent value. See below for transparent/solid values.

Parameters (1,2) address of the source rectangle data.

Parameters (3,4) address of the target rectangle data.

The source and target rectangle data is laid out in memory as follows:

0-2 24 bit address to copy from/to (address is address:page:0)

3 pad byte (must be zero)

4-5 Stride, in bytes. This is the value to add to the address to get from one line to the next. Used for both source and target. For example: - if blitting to the screen, a stride of screen width (320) would get to the next line. - a zero source stride would repeat a single line for the whole copy. - A negative target stride would draw from the bottom upward.

6 data format 0: bytes. Supported for both source and target. 1: pairs of 4-bit values (nibbles). Source only. 2: 8 single-bit values. Source only. 3: high nibble. Target only. 4: low nibble. Target only. 5: two pixels per byte, left pixel in the high nibble, the first pixel going to the HIGH nibble of the first byte (even x). Target only (Trinity T-90, the 16 colour pages of mode 2). 6: the same, the first pixel going to the LOW nibble of the first byte (odd x). Target only. With 5 and 6 every source format and action is supported ; each value keeps its low 4 bits (the solid value too) and a nibble that is not written is left unchanged.

7 A constant to use as the "transparent" value for BLTACT_MASK and BLTACT_SOLID. Source only. Not used in target.

8 A constant to use as the "solid" value for BLTACT_SOLID. Source only. Not used in target.

9 Height. The number of lines to copy. Source only. Not used in target. The copy is driven by the source height.

10-11 Width. The number of values to copy for each line. Source only. Not used in target. The copy is driven by the source width.

C (llvm-mos)
/* Déplier des pixels de 4 bits sur deux lignes de l'écran (groupe 12, fonction 3). */
#include <stdio.h>
#include <neo6502.h>
#include <neo/api.h>

/* Appel brut de la boîte aux lettres : fonction puis groupe (écrit en dernier, il lance l'appel). */
static void api(uint8_t groupe, uint8_t fonction) {
    ControlPort.function = fonction;
    ControlPort.command = groupe;
    while (ControlPort.command)        /* attendre la fin */
        ;
}
static void libre(void) {              /* attendre que la boîte aux lettres soit libre */
    while (ControlPort.command)
        ;
}

/* Descripteurs de zone (voir la fiche) : adresse sur 24 bits, 0, pas, format, transparent, uni, hauteur, largeur. */
static uint8_t pixels[4] = {0x12, 0x34, 0x56, 0x78};   /* 8 pixels de 4 bits : 1 à 8 */
static uint8_t source[12], cible[12];

int main(void) {
    putchar(12);
    source[0] = (uint16_t)pixels & 0xFF;        /* adresse en RAM du 6502 (page $00) */
    source[1] = (uint16_t)pixels >> 8;
    source[4] = 0;                              /* pas 0 : la même ligne resservira */
    source[6] = 1;                              /* format 1 : paires de 4 bits */
    source[9] = 2;                              /* hauteur : 2 lignes */
    source[10] = 8;                             /* largeur : 8 pixels */
    uint16_t ecran = 120 * 320 + 100;           /* ligne 120, x 100 */
    cible[0] = ecran & 0xFF;
    cible[1] = ecran >> 8;
    cible[2] = 0x80;                            /* page $80 : mémoire vidéo */
    cible[4] = 320 & 0xFF;                      /* pas : une ligne d'écran */
    cible[5] = 320 >> 8;
    cible[6] = 0;                               /* format 0 : un octet par pixel */
    libre();
    ControlPort.params[0] = 0;                  /* P0 : action 0, copie */
    ControlPort.params[1] = (uint16_t)source & 0xFF;   /* P1-P2 : zone source */
    ControlPort.params[2] = (uint16_t)source >> 8;
    ControlPort.params[3] = (uint16_t)cible & 0xFF;    /* P3-P4 : zone cible */
    ControlPort.params[4] = (uint16_t)cible >> 8;
    api(12, 3);                                 /* Complex Blit Copy */
    uint8_t l[2][8];
    for (uint8_t y = 0; y < 2; y++)
        for (uint8_t x = 0; x < 8; x++)
            l[y][x] = neo_graphics_read_pixel(100 + x, 120 + y);
    for (uint8_t y = 0; y < 2; y++) {
        printf("Ligne %u :", y + 1);
        for (uint8_t x = 0; x < 8; x++)
            printf(" %u", l[y][x]);
        printf("\n");
    }
    return 0;
}
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
Ligne 1 : 1 2 3 4 5 6 7 8
Ligne 2 : 1 2 3 4 5 6 7 8
Mad Pascal
// Déplier des pixels de 4 bits sur deux lignes de l'écran (groupe 12, fonction 3).
program blit3;
uses crt, neo6502;
const pixels: array[0..3] of byte = ($12, $34, $56, $78);   // 8 pixels de 4 bits : 1 à 8
var source, cible: array[0..11] of byte;        // descripteurs de zone (voir la fiche)
    l: array[0..15] of byte;
    x, y: byte;
begin
  write(chr(12));
  fillchar(@source, 12, 0);
  fillchar(@cible, 12, 0);
  source[0] := lo(word(@pixels));               // adresse en RAM du 6502 (page $00)
  source[1] := hi(word(@pixels));
  source[6] := 1;                               // format 1 : paires de 4 bits ; pas 0 : la même ligne resservira
  source[9] := 2;                               // hauteur : 2 lignes
  source[10] := 8;                              // largeur : 8 pixels
  cible[0] := lo(120 * 320 + 100);              // ligne 120, x 100
  cible[1] := hi(120 * 320 + 100);
  cible[2] := $80;                              // page $80 : mémoire vidéo
  cible[4] := lo(320);                          // pas : une ligne d'écran
  cible[5] := hi(320);                          // format 0 : un octet par pixel
  NeoBlitterXCopy(0, @source, @cible);          // Complex Blit Copy, action 0 : copie
  for y := 0 to 1 do
    for x := 0 to 7 do
      l[y * 8 + x] := NeoGetPixel(100 + x, 120 + y);
  for y := 0 to 1 do begin
    write('Ligne ', y + 1, ' :');
    for x := 0 to 7 do write(' ', l[y * 8 + x]);
    writeln;
  end;
end.
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
Ligne 1 : 1 2 3 4 5 6 7 8
Ligne 2 : 1 2 3 4 5 6 7 8
NeoBASIC
10 ' Deplier des pixels de 4 bits sur deux lignes de l'ecran (groupe 12, fonction 3)
20 cls
30 p = alloc(4) : poke p, $12 : poke p + 1, $34 : poke p + 2, $56 : poke p + 3, $78 : ' 8 pixels de 4 bits : 1 a 8
40 s = alloc(12) : c = alloc(12) : for i = 0 to 11 : poke s + i, 0 : poke c + i, 0 : next i
50 doke s, p : ' zone source : adresse en RAM du 6502 (page $00), pas 0 (la meme ligne resservira)
60 poke s + 6, 1 : poke s + 9, 2 : poke s + 10, 8 : ' format 1 (paires de 4 bits), hauteur 2, largeur 8
70 doke c, 38500 : poke c + 2, $80 : ' zone cible : 120 * 320 + 100 (ligne 120, x 100), page $80 (video)
80 doke c + 4, 320 : ' pas : une ligne d'ecran ; format 0, un octet par pixel
90 while peek($ff00) : wend
100 poke $ff04, 0 : doke $ff05, s : doke $ff07, c : poke $ff01, 3 : poke $ff00, 12 : ' Complex Blit Copy, action 0 : copie
110 while peek($ff00) : wend
120 for y = 0 to 1 : print "Ligne ";y + 1;" :"; : for x = 0 to 7 : print " ";point(100 + x, 120 + y); : next x : print : next y
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
Ligne 1 : 1 2 3 4 5 6 7 8
Ligne 2 : 1 2 3 4 5 6 7 8
65C02 assembly (64tass)
; Déplier des pixels de 4 bits sur deux lignes de l'écran (groupe 12, fonction 3).
        .cpu "65c02"
        * = $0800
ControlPort    = $FF00             ; +0 groupe, +1 fonction, +2 erreur, +4.. paramètres
WaitMessage    = $FFF4             ; attend que la boîte aux lettres soit libre
WriteCharacter = $FFF1             ; affiche le caractère de A

start:   lda #12                    ; écran effacé : on relit des pixels
        jsr WriteCharacter
        lda #<blit
        ldx #>blit
        jsr envoi
        ldx #0
_ll1:  phx
        txa
        clc
        adc #100
        ldy #120
        jsr pixel
        plx
        sta l1,x
        inx
        cpx #8
        bne _ll1
        ldx #0
_ll2:  phx
        txa
        clc
        adc #100
        ldy #121
        jsr pixel
        plx
        sta l2,x
        inx
        cpx #8
        bne _ll2
        lda #<t1
        ldx #>t1
        jsr ecrire
        ldx #0
_al1:  lda #' '
        jsr WriteCharacter
        lda l1,x
        jsr decimal
        inx
        cpx #8
        bne _al1
        lda #13
        jsr WriteCharacter
        lda #<t2
        ldx #>t2
        jsr ecrire
        ldx #0
_al2:  lda #' '
        jsr WriteCharacter
        lda l2,x
        jsr decimal
        inx
        cpx #8
        bne _al2
        lda #13
        jsr WriteCharacter
        rts

; Couleur du fond (5,33) en (A, Y), X et Y inférieurs à 256, rendue dans A.
pixel:  pha
        jsr WaitMessage
        pla
        sta ControlPort+4          ; P0-P1 : X
        stz ControlPort+5
        sty ControlPort+6          ; P2-P3 : Y
        stz ControlPort+7
        lda #5
        ldx #33                    ; 5,33 : Read Pixel
        jsr appel
        lda ControlPort+4
        rts

blit:   .byte 12, 3, 5, 0          ; 12,3 : Complex Blit Copy, action 0 (copie)
        .word source, cible        ; zones source et cible
pixels: .byte $12, $34, $56, $78   ; 8 pixels de 4 bits : 1 à 8
source: .word pixels              ; adresse en RAM du 6502...
        .byte 0, 0                 ; ... page $00, octet de bourrage
        .word 0                    ; pas 0 : la même ligne resservira
        .byte 1, 0, 0, 2           ; format 1 (paires de 4 bits), transparent, uni, hauteur 2
        .word 8                    ; largeur : 8 pixels
cible:  .word 120*320+100          ; ligne 120, x 100...
        .byte $80, 0               ; ... page $80 (mémoire vidéo)
        .word 320                  ; pas : une ligne d'écran
        .byte 0, 0, 0, 0           ; format 0 : un octet par pixel
        .word 0
t1:     .text "Ligne 1 :", 0
t2:     .text "Ligne 2 :", 0
l1:     .fill 8
l2:     .fill 8

; Appel décrit par un bloc dont l'adresse est dans A (poids faible) et X (poids fort) :
; groupe, fonction, nombre de paramètres, puis les paramètres (P0, P1...).
envoi:  sta blk
        stx blk+1
        jsr WaitMessage
        ldy #2
        lda (blk),y                ; nombre de paramètres
        clc
        adc #3
        sta fin
        ldy #3
_copie: cpy fin
        beq _lance
        lda (blk),y
        sta ControlPort+1,y        ; octet 3 du bloc -> P0 ($FF04)...
        iny
        bra _copie
_lance: ldy #1
        lda (blk),y
        tax
        lda (blk)
        jmp appel

; Affiche la chaîne terminée par 0 dont l'adresse est dans A (poids faible) et X (poids fort).
ecrire: sta ptr
        stx ptr+1
        ldy #0
_e:     lda (ptr),y
        beq _f
        jsr WriteCharacter
        iny
        bne _e
_f:     rts

; Affiche A (0-255) en décimal, sans zéros de tête.
decimal:
        phx
        phy
        ldx #0
        ldy #100
        jsr _chiffre
        ldy #10
        jsr _chiffre
        ora #'0'
        jsr WriteCharacter
        ply
        plx
        rts
_chiffre:
        sty diviseur
        ldy #0
_div:   cmp diviseur
        bcc _fini
        sbc diviseur
        iny
        bne _div
_fini:  pha
        tya
        bne _ecrit
        cpx #0
        beq _saute
_ecrit: ora #'0'
        jsr WriteCharacter
        ldx #1
_saute: pla
        rts

; Appel : A = groupe, X = fonction (les paramètres sont déjà en place).
appel:  stx ControlPort+1
        sta ControlPort            ; écrit en dernier : lance l'appel
        jmp WaitMessage

ptr      = $80
blk      = $82
diviseur: .byte 0
fin:     .byte 0
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
Ligne 1 : 1 2 3 4 5 6 7 8
Ligne 2 : 1 2 3 4 5 6 7 8

12,4 Blit Image Changed by Trinity

Draws an image (described like the source of 12,3) on the screen, clipping it at the edges.

Inputs
BytesTypeMeaning
P0uint8action, as for 12,3
P1-P2addraddress of the source area descriptor
P3-P4int16screen X (signed)
P5-P6int16screen Y (signed)
P7uint8write: 0 whole byte, 3 high half, 4 low half
Outputs
BytesTypeMeaning
erreuruint8non zero in mode 1 or 2, or for an unknown source format

Clipping works per source byte: a one-bit-per-pixel image drawn at x = -2 loses its whole first byte (8 pixels), so pixels 0 to 5 stay empty and the image starts at x = 6. This is what the example shows (action 2, colour 6). Trinity refuses the call in the packed modes 1 and 2, where the image would be written at the wrong place; 12,2 and 12,3 copy raw bytes there. NeoBASIC has no keyword for this call.

Firmware documentation text

Blits an image from memory onto the screen. The image will be clipped, so it's safe to blit partly (or fully) offscreen-images. Mode 0 only : in the packed modes (1 and 2) the error flag is set and nothing is drawn (Trinity T-90). Functions 12,2 and 12,3 copy raw bytes into video RAM in every mode ; in the packed modes a byte holds 2 (mode 2) or 8 (mode 1) pixels, and pages $80/$81 address the video RAM from its start, not the draw page of 5,11.

Parameter (0) is the blit action (see function 3, Complex Blit):

Parameters (1,2) address of the source rectangle data.

Parameters (3,4) x pixel coordinate on screen (signed 16 bit)

Parameters (5,6) y pixel coordinate on screen (signed 16 bit)

Parameter (7) destination format, determines how framebuffer will be written: 0: write to whole byte. 1: unsupported 2: unsupported 3: write to high nibble only. 4: write to low nibble only.

NOTE: clipping operates at byte resolution on the source data. So, for example, if you blit a 1-bit image (format 2) to an x-position of -2, then the whole first byte will be skipped leaving 6 empty pixels on the left. Same happens on the right - either the whole source byte is used, or it'll be skipped.

C (llvm-mos)
/* Poser une image d'un bit par pixel, rognée à gauche (groupe 12, fonction 4). */
#include <stdio.h>
#include <neo6502.h>
#include <neo/api.h>

/* Appel brut de la boîte aux lettres : fonction puis groupe (écrit en dernier, il lance l'appel). */
static void api(uint8_t groupe, uint8_t fonction) {
    ControlPort.function = fonction;
    ControlPort.command = groupe;
    while (ControlPort.command)        /* attendre la fin */
        ;
}
static void libre(void) {              /* attendre que la boîte aux lettres soit libre */
    while (ControlPort.command)
        ;
}

static uint8_t bits[2] = {0xFF, 0xFF};           /* une ligne de 16 pixels allumés */
static uint8_t source[12];

int main(void) {
    putchar(12);
    source[0] = (uint16_t)bits & 0xFF;
    source[1] = (uint16_t)bits >> 8;
    source[4] = 0;                              /* pas 0 : la même ligne sur toute la hauteur */
    source[6] = 2;                              /* format 2 : 8 pixels d'un bit par octet */
    source[7] = 0;                              /* valeur transparente */
    source[8] = 6;                              /* couleur des pixels à 1 (action 2) */
    source[9] = 4;                              /* hauteur */
    source[10] = 16;                            /* largeur */
    libre();
    ControlPort.params[0] = 2;                  /* P0 : action 2, couleur unie là où la source n'est pas transparente */
    ControlPort.params[1] = (uint16_t)source & 0xFF;   /* P1-P2 : zone source */
    ControlPort.params[2] = (uint16_t)source >> 8;
    ControlPort.params[3] = (uint8_t)-2;        /* P3-P4 : x = -2 (signé) */
    ControlPort.params[4] = 0xFF;
    ControlPort.params[5] = 100;                /* P5-P6 : y = 100 */
    ControlPort.params[6] = 0;
    ControlPort.params[7] = 0;                  /* P7 : écrit l'octet entier */
    api(12, 4);                                 /* Blit Image */
    uint8_t e = ControlPort.error;
    uint8_t p[16];
    for (uint8_t x = 0; x < 16; x++)
        p[x] = neo_graphics_read_pixel(x, 100);
    printf("Erreur %u ; x 0 a 15 :", e);
    for (uint8_t x = 0; x < 16; x++)
        printf(" %u", p[x]);
    printf("\n");
    return 0;
}
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
Erreur 0 ; x 0 a 15 : 0 0 0 0 0 0 6 6 6 6 6 6 6 6 0 0
Mad Pascal
// Poser une image d'un bit par pixel, rognée à gauche (groupe 12, fonction 4).
program blit4;
uses crt, neo6502;
const bits: array[0..1] of byte = ($FF, $FF);   // une ligne de 16 pixels allumés
var source: array[0..11] of byte;
    p: array[0..15] of byte;
    x, e: byte;
begin
  write(chr(12));
  fillchar(@source, 12, 0);
  source[0] := lo(word(@bits));
  source[1] := hi(word(@bits));
  source[6] := 2;                               // format 2 : 8 pixels d'un bit par octet ; pas 0
  source[7] := 0;                               // valeur transparente
  source[8] := 6;                               // couleur des pixels à 1 (action 2)
  source[9] := 4;                               // hauteur
  source[10] := 16;                             // largeur
  NeoBlitImage(2, @source, word(-2), 100, 0);   // Blit Image : action 2, x = -2, y = 100, octet entier
  NeoWaitMessage;
  e := NeoMessage.error;
  for x := 0 to 15 do p[x] := NeoGetPixel(x, 100);
  write('Erreur ', e, ' ; x 0 a 15 :');
  for x := 0 to 15 do write(' ', p[x]);
  writeln;
end.
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
Erreur 0 ; x 0 a 15 : 0 0 0 0 0 0 6 6 6 6 6 6 6 6 0 0
NeoBASIC
10 ' Poser une image d'un bit par pixel, rognee a gauche (groupe 12, fonction 4)
20 cls
30 b = alloc(2) : poke b, $ff : poke b + 1, $ff : ' une ligne de 16 pixels allumes
40 s = alloc(12) : for i = 0 to 11 : poke s + i, 0 : next i
50 doke s, b : poke s + 6, 2 : poke s + 8, 6 : ' format 2 (8 pixels d'un bit par octet), pas 0, couleur 6 pour les 1
60 poke s + 9, 4 : poke s + 10, 16 : ' hauteur 4, largeur 16
70 while peek($ff00) : wend
80 poke $ff04, 2 : doke $ff05, s : doke $ff07, $fffe : doke $ff09, 100 : poke $ff0b, 0 : ' Blit Image : action 2, x = -2 ($fffe), y = 100
90 poke $ff01, 4 : poke $ff00, 12
100 while peek($ff00) : wend : e = peek($ff02)
110 print "Erreur ";e;" ; x 0 a 15 :"; : for x = 0 to 15 : print " ";point(x, 100); : next x : print
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
Erreur 0 ; x 0 a 15 : 0 0 0 0 0 0 6 6 6 6 6 6 6 6 0 0
65C02 assembly (64tass)
; Poser une image d'un bit par pixel, rognée à gauche (groupe 12, fonction 4).
        .cpu "65c02"
        * = $0800
ControlPort    = $FF00             ; +0 groupe, +1 fonction, +2 erreur, +4.. paramètres
WaitMessage    = $FFF4             ; attend que la boîte aux lettres soit libre
WriteCharacter = $FFF1             ; affiche le caractère de A

start:   lda #12                    ; écran effacé : on relit des pixels
        jsr WriteCharacter
        lda #<image
        ldx #>image
        jsr envoi
        lda ControlPort+2
        sta e
        ldx #0
_lp:    phx
        txa
        ldy #100
        jsr pixel
        plx
        sta p,x
        inx
        cpx #16
        bne _lp
        lda #<t0
        ldx #>t0
        jsr ecrire
        lda e
        jsr decimal
        lda #<t1
        ldx #>t1
        jsr ecrire
        ldx #0
_ap:  lda #' '
        jsr WriteCharacter
        lda p,x
        jsr decimal
        inx
        cpx #16
        bne _ap
        lda #13
        jsr WriteCharacter
        rts

; Couleur du fond (5,33) en (A, Y), X et Y inférieurs à 256, rendue dans A.
pixel:  pha
        jsr WaitMessage
        pla
        sta ControlPort+4          ; P0-P1 : X
        stz ControlPort+5
        sty ControlPort+6          ; P2-P3 : Y
        stz ControlPort+7
        lda #5
        ldx #33                    ; 5,33 : Read Pixel
        jsr appel
        lda ControlPort+4
        rts

image:  .byte 12, 4, 8, 2          ; 12,4 : Blit Image, action 2 (couleur unie)
        .word source
        .word 65536-2, 100         ; x = -2 (signé), y = 100
        .byte 0                    ; octet entier
bits:   .byte $FF, $FF             ; une ligne de 16 pixels allumés
source: .word bits
        .byte 0, 0
        .word 0                    ; pas 0 : la même ligne sur toute la hauteur
        .byte 2, 0, 6, 4           ; format 2 (bits), transparent 0, couleur 6, hauteur 4
        .word 16                   ; largeur
t0:     .text "Erreur ", 0
t1:     .text " ; x 0 a 15 :", 0
e:      .byte 0
p:      .fill 16

; Appel décrit par un bloc dont l'adresse est dans A (poids faible) et X (poids fort) :
; groupe, fonction, nombre de paramètres, puis les paramètres (P0, P1...).
envoi:  sta blk
        stx blk+1
        jsr WaitMessage
        ldy #2
        lda (blk),y                ; nombre de paramètres
        clc
        adc #3
        sta fin
        ldy #3
_copie: cpy fin
        beq _lance
        lda (blk),y
        sta ControlPort+1,y        ; octet 3 du bloc -> P0 ($FF04)...
        iny
        bra _copie
_lance: ldy #1
        lda (blk),y
        tax
        lda (blk)
        jmp appel

; Affiche la chaîne terminée par 0 dont l'adresse est dans A (poids faible) et X (poids fort).
ecrire: sta ptr
        stx ptr+1
        ldy #0
_e:     lda (ptr),y
        beq _f
        jsr WriteCharacter
        iny
        bne _e
_f:     rts

; Affiche A (0-255) en décimal, sans zéros de tête.
decimal:
        phx
        phy
        ldx #0
        ldy #100
        jsr _chiffre
        ldy #10
        jsr _chiffre
        ora #'0'
        jsr WriteCharacter
        ply
        plx
        rts
_chiffre:
        sty diviseur
        ldy #0
_div:   cmp diviseur
        bcc _fini
        sbc diviseur
        iny
        bne _div
_fini:  pha
        tya
        bne _ecrit
        cpx #0
        beq _saute
_ecrit: ora #'0'
        jsr WriteCharacter
        ldx #1
_saute: pla
        rts

; Appel : A = groupe, X = fonction (les paramètres sont déjà en place).
appel:  stx ControlPort+1
        sta ControlPort            ; écrit en dernier : lance l'appel
        jmp WaitMessage

ptr      = $80
blk      = $82
diviseur: .byte 0
fin:     .byte 0
Output observed in the Phosphoneo emulator, Trinity firmware 0.16.69:
Erreur 0 ; x 0 a 15 : 0 0 0 0 0 0 6 6 6 6 6 6 6 6 0 0

← The Neo6502 API