Schnelle Echtzeitlupe

Eine schnelle Lupe kann man in den verschiedensten Programmen gut gebrauchen. Es gibt mehrere Möglichkeiten, eine Lupe zu programmieren. Eine einfache, aber recht langsame Methode besteht darin, den zu vergrößernden Ausschnitt mit der Funktion vro_cpyfm() pixelweise ,auseinanderzublit∂ten‘. Der Vorteil der Methode liegt darin, daß sie inallen Auflösungen funktioniert, derNach-teil: besitzt man keinen Blitterchip und kein NVDI, hakt es etwas bei der Vergrößerung, große Lupen werden sogar richtig träge.

Will man eine sehr schnelle Lupe, muß man, dachte ich mir, den Assembler bemühen, um jedes Bit einzeln zu vergrößern. Gesagt, getan, viele Bits wurden geshiftet und ausmaskiert, aber die Geschwindigkeit der STAD-Lupe war nicht zu erreichen. Beim experimentieren fiel mir dann auf, daß, wenn man byteweise und nicht wortweise vergrößert, es ja nur 256 verschiedene Möglichkeiten statt 65535 gibt; die könnte man sich doch auch schon vorher errechnen und dann beim Vergrößern einfach nur noch kopieren.

Erstellen der Vergrößerungstabellen

Das Prinzip ist klar: bei zweifacher Vergrößerung wird aus einem Byte ein Integer, bei vierfacher Vergrößerung ein Langwort, bei achtfacher Vergrößerung muß man das Quellbyte auf zwei Langworte erweitern. Die drei Arrays heißen sinnigerweise auch zweifach, vierfach und achtfach. Jedes Bit der möglichen 256 Bytes wird einzeln geprüft, ob es gesetzt ist oder nicht. Ist ein Bit gesetzt, werden in den Vergrößerungstabellen an der entsprechenden Stelle 2, 4 oder 8 Bits gesetzt (mittels OR-Verknüpfung), ansonsten gelöscht (mittels NOT-AND-Verknüpfung). Dazu werden die Arrays maske2, maske4 und maske8 benötigt, die die vergrößerten Bits enthalten. Das Erstellen der Tabellen erledigt die Funktion tabellen_init().

Groß, größer, am größten

Zu Anfang des Programms zuerst das Übliche: GEM Bescheid sagen, daß wir da sind, und ein Handle für das VDI besorgen. Der Funktion open_work() wird ein Zeiger auf einen MFDB übergeben, der auf die Bildschirmadresse initialisiert wird. Dies kann ganz einfach durch NULL-Setzen geschehen, da das GEM bei vro_cpyfm() dann automatisch alle zum Bildschirm gehörenden Parameter in der Struktur einträgt. Die Funktion init_mfdb() hat mit der Lupe eigentlich nichts zu tun, mit ihr kann man eine beliebige MFDB-Struktur füllen: anhand der Breite und Höhe des gewünschten Rasters wird der Speicherbedarf ausgerechnet (unter Beachtung der Zahl der Farbebenen), ein Flag legt fest, ob das Raster auf eine durch 256 teilbare Adresse gesetzt werden soll (falls man einen Setscreen() auf das Raster machen will). Wir holen uns ein Raster für das zu ladende Bild und zwei Raster für die Lupe. Ein Raster wird für den zu vergrößernden Ausschnitt, das andere für den vergrößerten Auschnitt benötigt. Maximal kann die Lupe den gesamten Bildschirm einnehmen. Schnell noch das Bild zum Vergrößern laden (irgendein *.DOO) aber wehe, es ist nicht dal.

Aufgerufen wird die Lupe über die Funktion lupe() in der folgenden Endlosschleife (Rechtsklick beendet). Übergabeparameter sind zwei Rasteradressen für Quelle (in unserem Fall das Bild) und Ziel (der Bildschirm), vier Integer, die das zu vergrößernde Rechteck beschreiben (X,Y,W,H, man könnte auch eine GRECT-Struktur benutzen), die beiden Zielkoordinaten, und natürlich der Vergrößerungsfaktor. Die Funktion lupe() füllt mit den Koordinatenangaben ein Array und kopiert den Quellausschnitt auf das erste Lupenraster. Da die Funktion vro_cpyfm() sich die Parameter zum 'Blitten' aus der MFDB-Struktur holt, wird die Wortbreite des Lupenrasters auf die Wortbreite des Quellrechtecks angepaßt, damit die Bytes alle hintereinander auf dem Raster liegen. Anhand des Faktors wird entschieden, welche Lupen-Subroutine aufgerufen wird, danach wird die Vergrößerung auf das Zielraster kopiert.

Die drei Routinen sublupe2(), sublupe4(), und sublupe8() arbeiten recht ähnlich. Jeweils eine Zeile wird vergrößert, indem das Quellbyte als Index in die Vergrößerungstabelle benutzt wird (Aha!), daher die hohe Geschwindigkeit der Lupe. Die vergrößerte Zeile wird dann entweder einmal oder dreimal untereinander kopiert. Bei achtfacher Vergrößerung wird die vergrößerte Zeil sechsmal kopiert und die letzte Zeile gelöscht, was zusammen mit der Maske ,0x7F‘ das weiße Gitter ergibt. Will man kein weißes Gitter, muß man die Zeile siebenmal kopieren und die Maske ,0xFF‘ benutzen (im Array maskeß//). Solche kurzen Routinen verleiten mich immer dazu, sie mir mit dem hervorragenden Pure-Debugger genauer in Assembler anzusehen, da man (auch bei Pure-C) immer noch etwas zum Optimieren finden kann. Und es fanden sich auch einige Ansatzpunkte: die Kopierschleifen, bei denen die Zähler nicht zur Indexberechnung gebraucht werden, wurden vom Compiler mit ,CMP‘ (beim Aufwärtszählen) bzw. mit ,TST‘ (beim Abwärtszählen bis 0) auf die Abbruchbedingung geprüft. Dabei werden unnötige Sprungbefehle verwendet, die sich mit einer Assembler-Schleife, die ,DBF‘ oder ,DBRA‘ (zählt bis -1 herunter) benutzt, vermeiden lassen. Die Geschwindigkeitsvorteile lassen sich aber nur ausnutzen, wenn man einen Blitterchip besitzt oder NVDI benutzt, da die meiste Zeit nicht zum Vergrößern, sondern zum 'Blitten' verbraucht wird, besonders wenn die Ausschnitte nicht auf Wortgrenzen liegen (was leider meist der Fall ist). Z.B. dauert die zweifache Vergrößerung von 320x200 Pixel 640x400 Pixel 0.04 Sek. in C und 0.035 Sek. in Assembler. Für die Assembler-Freaks gibt es also auch die entsprechenden Listings, die mit dem Pure-Assembler übersetzt wurden. Aber da eignet sich wohl jeder andere der EXPORT und IMPORT versteht. Das Vereinbaren der Funktionen als MODULE kann auch weggelassen werden, nur bindet der Linker dann immer alle drei Funktionen ein, auch wenn nur eine gebraucht wird.

Ein Nachteil soll aber nicht verschwiegen werden: die Lupen laufen nur in monochromer Auflösung, wobei die Bildschirmgröße allerdings egal ist (mit BIGSCREEN getestet). Wer Lust hat, kann sich ja mal mit den verschiedenen Anordnungen der Farbebenen bei Grafikkarten etc. auseinandersetzen. Die Funktion vr_tmfm(), die ja ein Raster in ein auflösungsunabhängiges Format umwandelt, stellte sich nämlich als großer Bremsklotz heraus. Ansonsten viel Spaß beim Vergrößern.

/*-----------------------------------------*/
/*                                         */
/*             Echtzeitlupe in C           */
/*       programmiert von Ulrich Witte     */
/*                mit PURE-C               */
/*                                         */
/*         (c) 1992 MAXON Computer         */
/*                                         */
/*-----------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <tos.h>
#include <aes.h>
#include <vdi.h>

#define min(a, b) (((a) < (b)) ? (a) : (b))
#define max(a, b) (((a) > (b)) ? (a) : (b))

#define FALSE 0 
#define TRUE 1

#define BREITE 32 
#define HOEHE 32

typedef unsigned char byte;/* Spart Tipparbeit */

            /* Globale Variablen */

unsigned maske2[] = {3,12,48,192,768,
        3072,12288,49152U}; 
unsigned long maske4[] = {0x0000000f,
            0x000000f0,
            0x00000f00,
            0x0000f000,
            0x000f0000,
            0x00f00000,
            0x0f000000,
            0xf0000000};

/*
unsigned long maske8[] = {0x00000000,0x000000ff,
        0x00000000,0x0000ff00, 
        0x00000000,0x00ff0000, 
        0x00000000,0xff000000, 
        0x000000ff,0x00000000, 
        0x0000ff00,0x00000000, 
        0x00ff0000,0x00000000, 
        0xff000000,0x00000000} ;

*/

unsigned long maske8[] = {0x00000000,0x0000007f,
        0x00000000,0x00007f00, 
        0x00000000,0x007f0000, 
        0x00000000,0x7f000000, 
        0x0000007f,0x00000000, 
        0x00007f00,0x00000000, 
        0x007f0000,0x00000000, 
        0x7f000000,0x00000000} ;

MFDB bildschirm,bild,lupe1,lupe2; 
int aes_handle,vdi_handle,work_out[57], work_out_ext[57]; 
unsigned zweifach[256]; 
unsigned long vierfach[256]; 
unsigned long achtfach[512]; 
int cw,ch,zw,zh;

        /* Prototypen */ 

void tabellen_init(void);
int init_mfdb(MFDB *block,int breite,int hoehe, int flag); 
int open_work(MFDB *form); 
int main(void);
void lupe(MFDB *quelle, MFDB *ziel,
        int qx, int qy, int qw, int qh, int zx, int zy, int faktor); 
void sublupe2(byte *src, unsigned *dst, int bytes, int lines); 
void sublupe4(byte *src, unsigned long *dst, int bytes, int lines); 
void sublupe8(byte *src, unsigned long *dst, int bytes, int lines); 
void nothing(byte *src, void *dst, int bytes, int lines);

int align(int x,int n);

int main(void)
{
    int i;
    int mx, my, mk; 

    appl_init();
    vdi_handle = open_work(&bildschirm); 
    init_mfdb(&bild,639,399,0);
    init_mfdb(&lupe1,work_out[0],work_out[1],0); init_mfdb(&lupe2,work_out[0],work_out[1],0);
        /* Das Bild muß ’DESK.DOO' heißen, */
        /* wenn keins da ist wird eben weiß */
        /* vergrößert (sieht man nur nicht viel) */ 
    i = Fopen("DESK.DOO",0); 
    if (i >= 0)
    {
        Fread(i, 32000L,bild.fd_addr);
        Fclose(i);
    }
    tabellen_init();
    graf_mouse(M_OFF,0); /* Maus verstecken */
    do 
    {
        graf_mkstate(&mx,&my,&mk,&i); 
        lupe(&bild,&bildschirm,
            min(mx,639 - BREITE),min(my,399 - HOEHE), 
            BREITE,HOEHE,0,50,2); 
        lupe(&bild,&bildschirm,
            min(mx,639 - BREITE),min(my,399 - HOEHE), 
            BREITE,HOEHE,128,50,4); 
        lupe(&bild,&bildschirm,
            min(mx,639 - BREITE),min(my,399 - HOEHE), 
            BREITE,HOEHE,320,50,8);
    } while(!(mk & 2));
    graf_mouse(M_ON,0); /* Maus wieder an */
    v_clsvwk(vdi_handle); /* und abmelden */
    appl_exit(); 
    return 0;
}

/*--------------------------------------------
    Funktion tabellen_init

    Aufgabe: Erstellt die Vergrößerungstabellen 
    für 2, 4 und 8-fache Vergrößerung

    Eingabe: nichts

    Ausgabe: nichts

    Besonderes: nichts 
----------------------------------------------*/

void tabellen_init(void)
{
    int i,j,k;

    for (i = 0 ; i < 256 ; i++) /* pro Buchstabe */ 
    {   /* 8 Bits testen */
        for (j = 1, k = 0; j < 256 ; j *= 2, k++)
        {
            if ((byte)i & j)    /* Bit gesetzt */
            {                   /* Stelle setzen */
                zweifach[i] |= maske2[k];
                vierfach[i] |= maske4[k];
                achtfach[i * 2] |= maske8[k * 2];
                achtfach[i *2+1]    |=  maske8[k *2+1];
            }
            else                /* Bit nicht gesetzt */
            {                   /* Stelle löschen */
                zweifach[i] &= ~maske2[k]; 
                vierfach[i] &= ~maske4[k]; 
                achtfach[i * 2] &= ~maske8[k * 2]; 
                achtfach[i * 2 + 1] &= ~maske8[k*2 + 1];
            }
        }
    }
}

/*-------------------------------------------------

    Funktion lupe

    Aufgabe:    Vorbereitung    der Vergrößerung

    Eingabe: -quelle: Zeiger auf Quellraster 
            -ziel:  Zeiger auf Zielraster
            -qx,qy: x, y - Koordinaten des Quellausschnitts 
            -qw,qh: Breite, Höhe des QuellausSchnitts 
            -zx,zy: x, y - Koordinaten des Zielbreichs 
            -faktor: Vergrößerungsfaktor (derzeit 2,4,8)

    Ausgabe: nichts

    Besonderes: nichts 
------------------------------------------------*/

void lupe(MFDB *quelle, MFDB *ziel,
        int qx, int qy, int qw, int qh, 
        int zx, int zy, int faktor)
{
    int xy[8];
    void (*vergroessern)() = nothing;
    /* Dummyfunktion laden: */
    /* falls falscher Faktor übergeben wird */
    /* nur RTS statt Bomben! */

    /* Breite auf Wortgrenze bringen */ 
    qw = align(qw,16);
    /* MFDB-Wortbreite korrigieren */ 
    lupe1.fd_wdwidth = (qw >> 4);
        /* Bitblit-Array für Quellraster */ 
    xy[0] = xy[2] = qx; 
    xy[1] = xy[3] = qy; 
    xy[4] = xy[5] = 0; 
    xy[2] += (xy[6] = qw - 1); 
    xy[3] += (xy[7] = qh - 1);
        /* und 'blitten' */ 
    vro_cpyfm(vdi_handle,3,xy,quelle,&lupel); 
    switch (faktor)
    {   /*  je Faktor entsprechende Funktion laden */
        case 2:
            vergroessern = sublupe2; 
            break; 
        case 4:
            vergroessern = sublupe4; 
            break; 
        case 8:
            vergroessern = sublupe8; 
            break;
    }
    vergroessern(lupel.fd_addr,lupe2.fd_addr,
            (qw >> 3) , qh) ;
        /* Wortbreite für Zielraster setzen */ 
    lupe2.fd_wdwidth = (qw » 4) * faktor;
        /* Bitblit-Array für Zielraster */ 
    xy[0] = xy[1] = 0; 
    xy[4] = xy[6] = zx;
    xy[5] = xy[7] = zy;
    xy[6] += (xy[2] = qw * faktor - 1);
    xy[7] += (xy[3] = qh * faktor - 1);
        /* Vergrößerung ins Zielraster blitten */
    vro_cpyfm(vdi_handle,3,xy,&lupe2,ziel);
}

/*-------------------------------------------------
    Funktion sublupe2

    Aufgabe: Ausschnitt in x- und y-Richtung

    von src nach dst zweifach vergrößern

    Eingabe:-src: Quelladresse 
            -dst: Zieladresse 
            -bytes: Breite in Bytes 
            -lines: Höhe in Pixelzeilen

    Ausgabe: nichts

    Besonderes: nichts 
-------------------------------------------------*/

void sublupe2(byte *src, unsigned *dst, int bytes, int lines)
{
    unsigned *nextline; int i,j,f;
    for (i = 0 ; i < lines ; i++)
    {
        nextline = dst;
        for (j = 0 ; j < bytes ; j++)
            *dst++ = zweifach[*src++]; 
        for (j = 0 ; j < bytes ; j++)
            *dst++ = *nextline++;

    }

}

/*-------------------------------------------------
    Funktion sublupe4

    Aufgabe: Ausschnitt in x- und y-Richtung

    von src nach dst vierfach vergrößern

    Eingabe:-src: Quelladresse 
            -dst: Zieladresse 
            -bytes: Breite in Bytes 
            -lines: Höhe in Pixelzeilen

    Ausgabe: nichts

    Besonderes: nichts 
-------------------------------------------------*/

void sublupe4(byte *src, unsigned long *dst, int bytes, int lines)
{
    unsigned long *nextline; 
    int i,j,k,f;
    for (i = 0 ; i < lines ; i++)
    {
        nextline = dst;
        for (j = 0 ; j < bytes ; j++)
            *dst++ = vierfach[*src++]; 
        for (j = 0 ; j <=2 ; j++)
        {
            for (k = 0 ; k < bytes ; k++)
                *dst++ = *nextline++; 
            nextline -= bytes;

        }
    }
}

/*-------------------------------------------------
    Funktion sublupe8

    Aufgabe:    Ausschnitt in x- und y-Richtung
                von src nach dst achtfach vergrößern

    Eingabe: -src: Quelladresse 
            -dst: Zieladresse
            -bytes: Breite in Bytes 
            -lines: Höhe in Pixelzeilen

Ausgabe: nichts

Besonderes: nichts 
-------—-----------------------------------------*/

void sublupe8(byte *src, unsigned long *dst, int bytes, int lines)
{
    unsigned long *nextline,*ptr; 
    int i,j,k;
    int offset = bytes * 2;

    for (i = 0 ; i < lines ; i++)
    {
        nextline = dst;
        for (j = 0 ; j < bytes ; j++)
        {
            ptr = &achtfach[(unsigned)(*src++) * 2]; 
            *dst++ = *ptr++;
            *dst++ = *ptr;
        }
        for (j = 0 ; j <= 5 ; j++)
        {
            for (k = 0 ; k < bytes ; k++)
            {
                *dst++ = *nextline++;
                *dst++ = *nextline++;
            }
            nextline -= offset;
        }
        for (k = 0 ; k < bytes ; k++)
        {
            *dst++ = 0L; /* weißes  Gitter  */
            *dst++ = 0L;
        }

    }
}

/*-----------------------------------------

Funktion align

Aufgabe:    x auf den nächsten durch
            n teilbaren Wert setzen

Eingabe:    -x: zu setzender Wert 
            -n: Teiler

Ausgabe: - x auf den nächsten durch n teilbaren Wert gesetzt

Besonderes: nichts 
-----------------------------------------*/

int align(int x,int n)
{
    x += (n >> 1) - 1; 
    x = n * (x / n) ; 
    return x;
}

/*-------------------------------------
    Funktion nothing

    Aufgabe: Dummy-Funktion
-------------------------------------*/

void nothing(byte *src, void *dst, int bytes, int lines)
{
    return;
}

/*--------------------------------------------
    Funktion open_work

    Aufgabe: GEM-Initialisierung, erweiterte Workstation-Info holen

    Eingabe:    -form: Zeiger auf MFBD-Strukur, die auf Bildschirm gesetzt wird

    Ausgabe: vdi_handle

    Besonderes: Programmabbruch, falls keine Workstation geöffnet werden kann

-------------------------------------------*/

int open_work(MFDB *form)
{
    int x;
    int work_in[11]; 
    int vdi_handle;

    for(x = 0; x < 10; work_in[x++] = 1)
        ;
    work_in[10] = 2;

    aes_handle = graf_handle(&zw,&zh,&cw,&ch); 
    vdi_handle = work_in[0] = aes_handle; 
    v_opnvwk(work_in, &vdi_ handle, work_out);

    if (vdi_handle == 0) /* keine Workstation */
    {
        Cconws("\033E Error: GEM-Initialisierung" "nicht möglich!");
        Bconin(2); 
        exit(1);
    }
    form->fd_addr = NULL;
        /* erweiterte Parameter für Farbebenen */ 
    vq_extnd(vdi_handle,1,work_out_ext); 
    return vdi_handle; /* alles OK */
}

/*--------------------------------------------

    Funktion init_mfdb

    Aufgabe: füllt eine MFDB-Struktur

    Eingabe:    -block: Zeiger auf MFDB-Strukur, die gefüllt wird 
                -breite: Rasterbreite in Pixel
                -hoehe: Rasterhöhe in Pixel

                -flag: TRUE = Rasteradresse auf durch 256 teilbare Adresse setzen (für Setscreen)

    Ausgabe: 1 = ok, 0 = Fehler

    Besonderes: nichts 
--------------------------------------------*/

int init_mfdb(MFDB *block,int breite,int hoehe, int flag)
{
    int farbebenen = work_out_ext[4]; 

    hoehe++;
    if(breite & 0xf)
        breite += (0x10 - breite & Oxf); 
    block->fd_addr = (char *)malloc( (flag ? 256L : 0L) + 
        ((long)hoehe * (long)(breite » 3) *
        (long)farbebenen)); 
    if (block->fd_addr == NULL) return FALSE;
        /* evtl. Adresse anpassen */ 
    if (flag)
        if ((long)block->fd_addr & 0xff)
            (long)block->fd_addr +=
            (0x100 - (long)block->fd_addr & Oxff); 
    block->fd_w = breite; 
    block->fd_h = hoehe; 
    block->fd_wdwidth = breite >> 4; 
    block->fd_stand = 0; 
    block->fd_nplanes = farbebenen; 
    return TRUE;
}


EXPORT sublupe2,sublupe4,sublupe8

IMPORT zweifach,vierfach,achtfach

; Parameter für alle 3 Routinen:
; A0 = Quelladresse,
; A1 = Zieladresse,
; D0 = Breite in Bytes 
; D1 = Zeilen in Pixel

TEXT

MODULE sublupe2:
            movem.l a2-a3/d3,-(a7) 
            lea zweifach,a3 
            subq #1,d0
            subq #1,d1
nextline:   movea.l a1,a2 
            move.w d0,d2 
nextbyte:   clr.w d3
            move.b (a0)+,d3 ; Quellbyte * 2 als 
                            ; Index in die 
                            ; Vergrößerungstabeile
            add.w d3,d3
            move.w (a3,d3.w),(a1)+ ; Wert kopieren 
            dbf d2,nextbyte 
            move.w d0,d2 
copyline:   move.w (a2)+,(a1)+  ; Zeile kopieren
            dbf d2,copyline 
            dbf d1,nextline 
            movem.l (a7)+,a2~a3/d3 
            rts
ENDMOD

MODULE sublupe4:
            movem.l a2-a3/d3-d4,-(a7)
            lea vierfach, a3
            move.w d0,d4    ; Abstand in der Tabelle
            ext.l   d4      ; (Zugriff auf  Longs)
            lsl.l   #2,d4   ; ausrechnen
            subq #1,d1 
            subq #1,d0
nextline:   movea.l a1,a2 
            move.w d0,d2 
nextbyte:   clr.w   d3
            move.b  (a0)+,d3    ; wie sublupe2
            add.w   d3,d3   ; Quellbyte * 4
            add.w   d3,d3
            move.l (a3,d3.w),(a1)+
            dbf     d2,nextbyte
            moveq   #2,d3
dreilines:  move.w d0,d2    ; Ergebnis 3 mal
                            ; kopieren 
copyline:   move.l  (a2)+,(a1)+ 
            dbf     d2,copyline 
            suba.l  d4,a2 
            dbf     d3,dreilines 
            dbf     d1,nextline 
            movem.l (a7)+,a2-a3/d3-d4 
            rts
ENDMOD

MODULE sublupe8:
            movem.l a2-a4/d3-d7,-(a7) 
            move.w d0,d2 
            move.w d0,d7 
            ext.l d2
            lsl.l #3,d2         ; Tabellenabstand 2 Longs 
            subq #1,d1 
            subq #1,d7
            lea achtfach,a3 
nextline:   movea.l a1,a2 
            move.w d7,d3 
nextbyte:   clr.w d6
            move.b (a0)+,d6
            add.w d6,d6         ; Quellbyte * 8 1
            add.w d6,d6
            add.w d6,d6
            lea (a3,d6.w),a4
            move.l (a4)+,(a1)+  ; 2 Longs Ergebnis
            move.l (a4)/(a1)+   ; kopieren
            dbf d3,nextbyte
            moveq #5,d4         ; 5 Zeilen + 1 weiße
                                ; oder 6 Zeilen kopieren 
                                ; (je nach Maske)
fivelines:  move.w d7,d3 
copyline:   move.l (a2)+,(a1)+ 
            move.l (a2)+,(a1)+ 
            dbf d3,copyline 
            suba.l d2,a2 
            dbf d4,fivelines 
            move.w d7,d0        ; weiße Zeile
clearline:  clr.1 (a1)+
            clr.l (a1)+ 
            dbf d0,clearline 
            dbf d1,nextline 
            movem.l (a7)+,a2-a4/d3-d7 
            rts
ENDMOD

END


Ulrich Witte



Links

Copyright-Bestimmungen: siehe Über diese Seite
Classic Computer Magazines
[ Join Now | Ring Hub | Random | << Prev | Next >> ]