2024-02-21 16:52:06 +00:00
/ * *
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License , Version 2 . 0 ( the " License " ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an " AS IS " BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* * /
2017-12-28 16:40:15 +00:00
package jace.core ;
import jace.Emulator ;
import jace.config.ConfigurableField ;
import jace.config.InvokableAction ;
2023-07-03 20:44:23 +00:00
import jace.state.Stateful ;
2017-12-28 16:40:15 +00:00
import javafx.scene.image.Image ;
import javafx.scene.image.WritableImage ;
/ * *
* Generic abstraction of a 560x192 video output device which renders 40 columns
* per scanline . This also triggers VBL and updates the physical screen .
* Subclasses are used to manage actual rendering via ScreenWriter
* implementations . Created on November 10 , 2006 , 4 : 29 PM
*
* @author Brendan Robert ( BLuRry ) brendan . robert @gmail.com
* /
@Stateful
2024-02-21 16:52:06 +00:00
public abstract class Video extends TimedDevice {
2017-12-28 16:40:15 +00:00
@Stateful
WritableImage video ;
WritableImage visible ;
VideoWriter currentWriter ;
private byte floatingBus = 0 ;
private int width = 560 ;
private int height = 192 ;
@Stateful
public int x = 0 ;
@Stateful
public int y = 0 ;
@Stateful
public int scannerAddress ;
@Stateful
public int vPeriod = 0 ;
@Stateful
public int hPeriod = 0 ;
static final public int CYCLES_PER_LINE = 65 ;
static final public int TOTAL_LINES = 262 ;
static final public int APPLE_CYCLES_PER_LINE = 40 ;
static final public int APPLE_SCREEN_LINES = 192 ;
static final public int HBLANK = CYCLES_PER_LINE - APPLE_CYCLES_PER_LINE ;
static final public int VBLANK = ( TOTAL_LINES - APPLE_SCREEN_LINES ) * CYCLES_PER_LINE ;
2021-11-09 17:20:19 +00:00
static final public int [ ] textOffset = new int [ 192 ] ;
static final public int [ ] hiresOffset = new int [ 192 ] ;
static final public int [ ] textRowLookup = new int [ 0x0400 ] ;
static final public int [ ] hiresRowLookup = new int [ 0x02000 ] ;
private boolean screenDirty = true ;
private boolean lineDirty = true ;
2017-12-28 16:40:15 +00:00
private boolean isVblank = false ;
2021-11-09 17:20:19 +00:00
static void initLookupTables ( ) {
2017-12-28 16:40:15 +00:00
for ( int i = 0 ; i < 192 ; i + + ) {
textOffset [ i ] = calculateTextOffset ( i > > 3 ) ;
hiresOffset [ i ] = calculateHiresOffset ( i ) ;
}
for ( int i = 0 ; i < 0x0400 ; i + + ) {
textRowLookup [ i ] = identifyTextRow ( i ) ;
}
for ( int i = 0 ; i < 0x2000 ; i + + ) {
hiresRowLookup [ i ] = identifyHiresRow ( i ) ;
}
}
private int forceRedrawRowCount = 0 ;
/ * *
* Creates a new instance of Video
*
* @param computer
* /
2024-02-10 05:50:53 +00:00
public Video ( ) {
2024-02-21 16:52:06 +00:00
super ( ) ;
2021-11-09 17:20:19 +00:00
initLookupTables ( ) ;
2017-12-28 16:40:15 +00:00
video = new WritableImage ( 560 , 192 ) ;
visible = new WritableImage ( 560 , 192 ) ;
vPeriod = 0 ;
hPeriod = 0 ;
_forceRefresh ( ) ;
}
public void setWidth ( int w ) {
width = w ;
}
public int getWidth ( ) {
return width ;
}
public void setHeight ( int h ) {
height = h ;
}
public int getHeight ( ) {
return height ;
}
public VideoWriter getCurrentWriter ( ) {
return currentWriter ;
}
public void setCurrentWriter ( VideoWriter currentWriter ) {
if ( this . currentWriter ! = currentWriter | | currentWriter . isMixed ( ) ) {
this . currentWriter = currentWriter ;
forceRedrawRowCount = APPLE_SCREEN_LINES + 1 ;
}
}
@ConfigurableField ( category = " video " , name = " Min. Screen Refesh " , defaultValue = " 15 " , description = " Minimum number of miliseconds to wait before trying to redraw. " )
public static int MIN_SCREEN_REFRESH = 15 ;
Runnable redrawScreen = ( ) - > {
2017-12-29 07:50:53 +00:00
if ( visible ! = null & & video ! = null ) {
2024-03-05 06:06:47 +00:00
screenDirty = false ;
visible . getPixelWriter ( ) . setPixels ( 0 , 0 , 560 , 192 , video . getPixelReader ( ) , 0 , 0 ) ;
2017-12-28 16:40:15 +00:00
}
} ;
public void redraw ( ) {
javafx . application . Platform . runLater ( redrawScreen ) ;
}
public void vblankStart ( ) {
if ( screenDirty & & isRunning ( ) ) {
redraw ( ) ;
}
}
abstract public void vblankEnd ( ) ;
abstract public void hblankStart ( WritableImage screen , int y , boolean isDirty ) ;
public void setScannerLocation ( int loc ) {
scannerAddress = loc ;
}
@Override
public void tick ( ) {
2022-03-04 02:10:20 +00:00
addWaitCycles ( waitsPerCycle ) ;
2023-08-02 04:04:58 +00:00
if ( y < APPLE_SCREEN_LINES ) setScannerLocation ( currentWriter . getYOffset ( y ) ) ;
2024-02-10 05:50:53 +00:00
setFloatingBus ( getMemory ( ) . readRaw ( scannerAddress + x ) ) ;
2017-12-28 16:40:15 +00:00
if ( hPeriod > 0 ) {
hPeriod - - ;
if ( hPeriod = = 0 ) {
x = - 1 ;
}
} else {
2024-02-10 05:50:53 +00:00
int xVal = x ;
if ( ! isVblank & & xVal < APPLE_CYCLES_PER_LINE & & xVal > = 0 ) {
draw ( xVal ) ;
2017-12-28 16:40:15 +00:00
}
2024-02-10 05:50:53 +00:00
if ( xVal > = APPLE_CYCLES_PER_LINE - 1 ) {
2017-12-28 16:40:15 +00:00
int yy = y + hblankOffsetY ;
if ( yy < 0 ) {
yy + = APPLE_SCREEN_LINES ;
}
if ( yy > = APPLE_SCREEN_LINES ) {
yy - = ( TOTAL_LINES - APPLE_SCREEN_LINES ) ;
}
x = hblankOffsetX - 1 ;
if ( ! isVblank ) {
if ( lineDirty ) {
screenDirty = true ;
currentWriter . clearDirty ( y ) ;
}
hblankStart ( video , y , lineDirty ) ;
lineDirty = false ;
forceRedrawRowCount - - ;
}
hPeriod = HBLANK ;
y + + ;
2024-03-13 05:04:15 +00:00
getCurrentWriter ( ) . setCurrentRow ( y ) ;
2017-12-28 16:40:15 +00:00
if ( y > = APPLE_SCREEN_LINES ) {
if ( ! isVblank ) {
y = APPLE_SCREEN_LINES - ( TOTAL_LINES - APPLE_SCREEN_LINES ) ;
isVblank = true ;
vblankStart ( ) ;
2024-02-10 05:50:53 +00:00
Emulator . withComputer ( c - > c . getMotherboard ( ) . vblankStart ( ) ) ;
2017-12-28 16:40:15 +00:00
} else {
y = 0 ;
isVblank = false ;
vblankEnd ( ) ;
2024-02-10 05:50:53 +00:00
Emulator . withComputer ( c - > c . getMotherboard ( ) . vblankEnd ( ) ) ;
2017-12-28 16:40:15 +00:00
}
}
}
}
x + + ;
}
abstract public void configureVideoMode ( ) ;
protected static int byteDoubler ( byte b ) {
int num
= // Skip hi-bit because it's not used in display
// ((b&0x080)<<7) |
( ( b & 0x040 ) < < 6 )
| ( ( b & 0x020 ) < < 5 )
| ( ( b & 0x010 ) < < 4 )
| ( ( b & 0x08 ) < < 3 )
| ( ( b & 0x04 ) < < 2 )
| ( ( b & 0x02 ) < < 1 )
| ( b & 0x01 ) ;
return num | ( num < < 1 ) ;
}
@ConfigurableField ( name = " Waits per cycle " , category = " Advanced " , description = " Adjust the delay for the scanner " )
public static int waitsPerCycle = 0 ;
@ConfigurableField ( name = " Hblank X offset " , category = " Advanced " , description = " Adjust where the hblank period starts relative to the start of the line " )
public static int hblankOffsetX = - 29 ;
@ConfigurableField ( name = " Hblank Y offset " , category = " Advanced " , description = " Adjust which line the HBLANK starts on (0=current, 1=next, etc) " )
public static int hblankOffsetY = 1 ;
2024-02-10 05:50:53 +00:00
private void draw ( int xVal ) {
2017-12-28 16:40:15 +00:00
if ( lineDirty | | forceRedrawRowCount > 0 | | currentWriter . isRowDirty ( y ) ) {
lineDirty = true ;
2024-02-10 05:50:53 +00:00
currentWriter . displayByte ( video , xVal , y , textOffset [ y ] , hiresOffset [ y ] ) ;
2017-12-28 16:40:15 +00:00
}
doPostDraw ( ) ;
}
static public int calculateHiresOffset ( int y ) {
return calculateTextOffset ( y > > 3 ) + ( ( y & 7 ) < < 10 ) ;
}
static public int calculateTextOffset ( int y ) {
return ( ( y & 7 ) < < 7 ) + 40 * ( y > > 3 ) ;
}
static public int identifyTextRow ( int y ) {
//floor((x-1024)/128) + floor(((x-1024)%128)/40)*8
// Caller must check result is <= 23, if so then they are in a screenhole!
return ( y > > 7 ) + ( ( ( y & 0x7f ) / 40 ) < < 3 ) ;
}
static public int identifyHiresRow ( int y ) {
int blockOffset = identifyTextRow ( y & 0x03ff ) ;
// Caller must check results is > 0, if not then they are in a screenhole!
if ( blockOffset > 23 ) {
return - 1 ;
}
return ( ( y > > 10 ) & 7 ) + ( blockOffset < < 3 ) ;
}
public abstract void doPostDraw ( ) ;
public byte getFloatingBus ( ) {
return floatingBus ;
}
private void setFloatingBus ( byte floatingBus ) {
this . floatingBus = floatingBus ;
}
@InvokableAction ( name = " Refresh screen " ,
category = " display " ,
description = " Marks screen contents as changed, forcing full screen redraw " ,
alternatives = " redraw " ,
defaultKeyMapping = { " ctrl+shift+r " } )
2021-11-09 17:20:19 +00:00
public static void forceRefresh ( ) {
2023-07-03 20:44:23 +00:00
Emulator . withVideo ( v - > v . _forceRefresh ( ) ) ;
2017-12-28 16:40:15 +00:00
}
2024-04-23 15:08:38 +00:00
protected void _forceRefresh ( ) {
2017-12-28 16:40:15 +00:00
lineDirty = true ;
screenDirty = true ;
forceRedrawRowCount = APPLE_SCREEN_LINES + 1 ;
}
@Override
public String getShortName ( ) {
return " vid " ;
}
public Image getFrameBuffer ( ) {
return visible ;
}
}