2019-10-05 01:34:15 +00:00
//
// Video.cpp
// Clock Signal
//
// Created by Thomas Harte on 04/10/2019.
// Copyright © 2019 Thomas Harte. All rights reserved.
//
# include "Video.hpp"
2019-11-09 20:31:41 +00:00
# include "../../../Outputs/Log.hpp"
2019-10-10 03:01:11 +00:00
2019-10-09 01:18:08 +00:00
# include <algorithm>
2019-11-13 00:30:28 +00:00
# include <cstring>
2019-10-09 01:18:08 +00:00
2019-10-05 01:34:15 +00:00
using namespace Atari : : ST ;
2019-10-09 01:18:08 +00:00
namespace {
2019-11-06 04:02:25 +00:00
/*!
Defines the line counts at which mode - specific events will occur :
vertical enable being set and being reset , and the line on which
the frame will end .
*/
2019-11-09 01:46:24 +00:00
const struct VerticalParams {
2019-11-06 04:02:25 +00:00
const int set_enable ;
const int reset_enable ;
const int height ;
} vertical_params [ 3 ] = {
2019-11-08 00:55:49 +00:00
{ 63 , 263 , 313 } , // 47 rather than 63 on early machines.
2019-12-08 16:51:12 +00:00
{ 34 , 234 , 263 } ,
2019-11-06 04:02:25 +00:00
{ 1 , 401 , 500 } // 72 Hz mode: who knows?
} ;
2019-10-09 01:18:08 +00:00
2019-11-06 04:02:25 +00:00
/// @returns The correct @c VerticalParams for output at @c frequency.
2019-11-18 02:28:51 +00:00
const VerticalParams & vertical_parameters ( Video : : FieldFrequency frequency ) {
2019-11-06 04:02:25 +00:00
return vertical_params [ int ( frequency ) ] ;
}
2019-10-09 01:18:08 +00:00
2019-12-12 04:22:20 +00:00
# define CYCLE(x) ((x) * 2)
2019-11-06 04:02:25 +00:00
/*!
Defines the horizontal counts at which mode - specific events will occur :
horizontal enable being set and being reset , blank being set and reset , and the
intended length of this ine .
2019-10-09 01:18:08 +00:00
2019-11-06 04:02:25 +00:00
The caller should :
2019-10-09 01:18:08 +00:00
2019-11-06 04:02:25 +00:00
* latch line length at cycle 54 ( TODO : also for 72 Hz mode ? ) ;
* at ( line length - 50 ) , start sync and reset enable ( usually for the second time ) ;
* at ( line length - 10 ) , disable sync .
*/
2019-11-09 01:46:24 +00:00
const struct HorizontalParams {
2019-11-06 04:02:25 +00:00
const int set_enable ;
const int reset_enable ;
2019-10-09 01:18:08 +00:00
2019-11-06 04:02:25 +00:00
const int set_blank ;
const int reset_blank ;
2019-10-09 01:18:08 +00:00
2019-11-06 04:02:25 +00:00
const int length ;
2019-11-09 01:46:24 +00:00
} horizontal_params [ 3 ] = {
2019-12-12 04:22:20 +00:00
{ CYCLE ( 56 ) , CYCLE ( 376 ) , CYCLE ( 450 ) , CYCLE ( 28 ) , CYCLE ( 512 ) } ,
{ CYCLE ( 52 ) , CYCLE ( 372 ) , CYCLE ( 450 ) , CYCLE ( 24 ) , CYCLE ( 508 ) } ,
{ CYCLE ( 4 ) , CYCLE ( 164 ) , CYCLE ( 999 ) , CYCLE ( 999 ) , CYCLE ( 224 ) } // 72Hz mode doesn't set or reset blank.
2019-10-09 01:18:08 +00:00
} ;
2019-11-18 02:28:51 +00:00
const HorizontalParams & horizontal_parameters ( Video : : FieldFrequency frequency ) {
2019-11-09 01:46:24 +00:00
return horizontal_params [ int ( frequency ) ] ;
2019-10-09 01:18:08 +00:00
}
2019-11-09 01:46:24 +00:00
# ifndef NDEBUG
struct Checker {
Checker ( ) {
for ( int c = 0 ; c < 3 ; + + c ) {
// Expected horizontal order of events: reset blank, enable display, disable display, enable blank (at least 50 before end of line), end of line
2019-11-18 02:28:51 +00:00
const auto horizontal = horizontal_parameters ( Video : : FieldFrequency ( c ) ) ;
2019-12-11 03:17:57 +00:00
if ( c < 2 ) {
assert ( horizontal . reset_blank < horizontal . set_enable ) ;
assert ( horizontal . set_enable < horizontal . reset_enable ) ;
assert ( horizontal . reset_enable < horizontal . set_blank ) ;
assert ( horizontal . set_blank + 50 < horizontal . length ) ;
} else {
assert ( horizontal . set_enable < horizontal . reset_enable ) ;
assert ( horizontal . set_enable + 50 < horizontal . length ) ;
}
2019-11-09 01:46:24 +00:00
// Expected vertical order of events: reset blank, enable display, disable display, enable blank (at least 50 before end of line), end of line
2019-11-18 02:28:51 +00:00
const auto vertical = vertical_parameters ( Video : : FieldFrequency ( c ) ) ;
2019-11-09 01:46:24 +00:00
assert ( vertical . set_enable < vertical . reset_enable ) ;
assert ( vertical . reset_enable < vertical . height ) ;
}
}
} checker ;
# endif
2020-01-03 04:33:35 +00:00
const int de_delay_period = CYCLE ( 28 ) ; // Amount of time after DE that observed DE changes. NB: HACK HERE. This currently incorporates the MFP recognition delay. MUST FIX.
2019-12-29 22:37:09 +00:00
const int vsync_x_position = CYCLE ( 56 ) ; // Horizontal cycle on which vertical sync changes happen.
2019-12-28 15:36:50 +00:00
const int hsync_start = CYCLE ( 48 ) ; // Cycles before end of line when hsync starts.
const int hsync_end = CYCLE ( 8 ) ; // Cycles before end of line when hsync ends.
2019-12-30 00:03:08 +00:00
const int hsync_delay_period = hsync_end ; // Signal hsync at the end of the line.
const int vsync_delay_period = hsync_delay_period ; // Signal vsync with the same delay as hsync.
2019-11-20 01:13:47 +00:00
// "VSYNC starts 104 cycles after the start of the previous line's HSYNC, so that's 4 cycles before DE would be activated. ";
2019-12-29 22:37:09 +00:00
// that's an inconsistent statement since it would imply VSYNC at +54, which is 2 cycles before DE in 60Hz mode and 6 before
// in 50Hz mode. I've gone with 56, to be four cycles ahead of DE in 50Hz mode.
2019-11-19 03:12:24 +00:00
2019-10-09 01:18:08 +00:00
}
2019-10-05 01:34:15 +00:00
Video : : Video ( ) :
2019-12-14 00:57:54 +00:00
deferrer_ ( [ = ] ( HalfCycles duration ) { advance ( duration ) ; } ) ,
2019-11-11 02:39:40 +00:00
crt_ ( 1024 , 1 , Outputs : : Display : : Type : : PAL50 , Outputs : : Display : : InputDataType : : Red4Green4Blue4 ) ,
2019-12-28 00:03:10 +00:00
video_stream_ ( crt_ , palette_ ) {
2019-11-08 01:02:45 +00:00
2019-11-09 20:00:42 +00:00
// Show a total of 260 lines; a little short for PAL but a compromise between that and the ST's
// usual output height of 200 lines.
2020-01-03 04:33:35 +00:00
crt_ . set_visible_area ( crt_ . get_rect_for_area ( 33 , 260 , 220 , 850 , 4.0f / 3.0f ) ) ;
2019-10-05 01:34:15 +00:00
}
2019-11-06 04:02:25 +00:00
void Video : : set_ram ( uint16_t * ram , size_t size ) {
2019-10-11 02:46:58 +00:00
ram_ = ram ;
}
2019-10-05 01:34:15 +00:00
void Video : : set_scan_target ( Outputs : : Display : : ScanTarget * scan_target ) {
crt_ . set_scan_target ( scan_target ) ;
}
2019-12-21 01:49:14 +00:00
void Video : : set_display_type ( Outputs : : Display : : DisplayType display_type ) {
crt_ . set_display_type ( display_type ) ;
}
2019-10-05 01:34:15 +00:00
void Video : : run_for ( HalfCycles duration ) {
2019-12-14 00:57:54 +00:00
deferrer_ . run_for ( duration ) ;
}
void Video : : advance ( HalfCycles duration ) {
2019-11-06 04:02:25 +00:00
const auto horizontal_timings = horizontal_parameters ( field_frequency_ ) ;
const auto vertical_timings = vertical_parameters ( field_frequency_ ) ;
int integer_duration = int ( duration . as_integral ( ) ) ;
2019-11-18 04:28:00 +00:00
// Effect any changes in visible state out here; they're not relevant in the inner loop.
if ( ! pending_events_ . empty ( ) ) {
auto erase_iterator = pending_events_ . begin ( ) ;
int duration_remaining = integer_duration ;
while ( erase_iterator ! = pending_events_ . end ( ) ) {
erase_iterator - > delay - = duration_remaining ;
2019-11-19 03:12:24 +00:00
if ( erase_iterator - > delay < = 0 ) {
2019-11-18 04:28:00 +00:00
duration_remaining = - erase_iterator - > delay ;
erase_iterator - > apply ( public_state_ ) ;
+ + erase_iterator ;
} else {
break ;
}
}
if ( erase_iterator ! = pending_events_ . begin ( ) ) {
pending_events_ . erase ( pending_events_ . begin ( ) , erase_iterator ) ;
}
}
2019-10-09 01:18:08 +00:00
while ( integer_duration ) {
2019-11-06 04:02:25 +00:00
// Seed next event to end of line.
int next_event = line_length_ ;
// Check the explicitly-placed events.
2019-11-09 03:18:47 +00:00
if ( horizontal_timings . reset_blank > x_ ) next_event = std : : min ( next_event , horizontal_timings . reset_blank ) ;
if ( horizontal_timings . set_blank > x_ ) next_event = std : : min ( next_event , horizontal_timings . set_blank ) ;
if ( horizontal_timings . reset_enable > x_ ) next_event = std : : min ( next_event , horizontal_timings . reset_enable ) ;
if ( horizontal_timings . set_enable > x_ ) next_event = std : : min ( next_event , horizontal_timings . set_enable ) ;
2019-12-11 02:24:15 +00:00
if ( next_load_toggle_ > x_ ) next_event = std : : min ( next_event , next_load_toggle_ ) ;
2019-11-06 04:02:25 +00:00
// Check for events that are relative to existing latched state.
2019-12-12 04:22:20 +00:00
if ( line_length_ - hsync_start > x_ ) next_event = std : : min ( next_event , line_length_ - hsync_start ) ;
if ( line_length_ - hsync_end > x_ ) next_event = std : : min ( next_event , line_length_ - hsync_end ) ;
2019-11-09 03:18:47 +00:00
// Also, a vertical sync event might intercede.
2019-11-20 01:13:47 +00:00
if ( vertical_ . sync_schedule ! = VerticalState : : SyncSchedule : : None & & x_ < vsync_x_position & & next_event > = vsync_x_position ) {
next_event = vsync_x_position ;
2019-11-09 03:18:47 +00:00
}
2019-11-06 04:02:25 +00:00
// Determine current output mode and number of cycles to output for.
2019-11-09 03:18:47 +00:00
const int run_length = std : : min ( integer_duration , next_event - x_ ) ;
2019-11-18 04:28:00 +00:00
const bool display_enable = vertical_ . enable & & horizontal_ . enable ;
2019-12-29 22:37:09 +00:00
const bool hsync = horizontal_ . sync ;
2019-12-30 00:03:08 +00:00
const bool vsync = vertical_ . sync ;
2019-11-06 04:02:25 +00:00
2019-12-13 04:20:28 +00:00
// Ensure proper fetching irrespective of the output.
if ( load_ ) {
const int since_load = x_ - load_base_ ;
// There will be pixels this line, subject to the shifter pipeline.
// Divide into 8-[half-]cycle windows; at the start of each window fetch a word,
// and during the rest of the window, shift out.
int start_column = since_load > > 3 ;
const int end_column = ( since_load + run_length ) > > 3 ;
while ( start_column ! = end_column ) {
data_latch_ [ data_latch_position_ ] = ram_ [ current_address_ & 262143 ] ;
data_latch_position_ = ( data_latch_position_ + 1 ) & 127 ;
+ + current_address_ ;
+ + start_column ;
}
}
2019-11-06 04:02:25 +00:00
if ( horizontal_ . sync | | vertical_ . sync ) {
2019-12-28 03:47:34 +00:00
video_stream_ . output ( run_length , VideoStream : : OutputMode : : Sync ) ;
2019-11-06 04:02:25 +00:00
} else if ( horizontal_ . blank | | vertical_ . blank ) {
2019-12-28 03:47:34 +00:00
video_stream_ . output ( run_length , VideoStream : : OutputMode : : Blank ) ;
2019-12-11 02:24:15 +00:00
} else if ( ! load_ ) {
2019-12-28 03:47:34 +00:00
video_stream_ . output ( run_length , VideoStream : : OutputMode : : Pixels ) ;
2019-11-06 04:02:25 +00:00
} else {
2019-12-11 02:24:15 +00:00
const int since_load = x_ - load_base_ ;
2019-11-11 02:39:40 +00:00
// There will be pixels this line, subject to the shifter pipeline.
// Divide into 8-[half-]cycle windows; at the start of each window fetch a word,
// and during the rest of the window, shift out.
2019-12-11 02:24:15 +00:00
int start_column = since_load > > 3 ;
const int end_column = ( since_load + run_length ) > > 3 ;
2019-11-11 02:39:40 +00:00
// Rules obeyed below:
//
// Video fetches occur as the first act of business in a column. Each
// fetch is then followed by 8 shift clocks. Whether or not the shifter
// was reloaded by the fetch depends on the FIFO.
if ( start_column = = end_column ) {
2019-12-28 03:47:34 +00:00
video_stream_ . output ( run_length , VideoStream : : OutputMode : : Pixels ) ;
2019-11-11 02:39:40 +00:00
} else {
// Continue the current column if partway across.
2019-12-11 02:24:15 +00:00
if ( since_load & 7 ) {
2019-11-11 02:39:40 +00:00
// If at least one column boundary is crossed, complete this column.
2019-12-28 03:47:34 +00:00
video_stream_ . output ( 8 - ( since_load & 7 ) , VideoStream : : OutputMode : : Pixels ) ;
2019-11-11 02:39:40 +00:00
+ + start_column ; // This starts a new column, so latch a new word.
2019-12-13 04:20:28 +00:00
push_latched_data ( ) ;
2019-11-11 02:39:40 +00:00
}
2019-10-09 01:18:08 +00:00
2019-11-11 02:39:40 +00:00
// Run for all columns that have their starts in this time period.
int complete_columns = end_column - start_column ;
while ( complete_columns - - ) {
2019-12-28 03:47:34 +00:00
video_stream_ . output ( 8 , VideoStream : : OutputMode : : Pixels ) ;
2019-12-13 04:20:28 +00:00
push_latched_data ( ) ;
2019-10-11 02:46:58 +00:00
}
2019-11-11 02:39:40 +00:00
// Output the start of the next column, if necessary.
2019-12-11 02:24:15 +00:00
if ( ( since_load + run_length ) & 7 ) {
2019-12-28 03:47:34 +00:00
video_stream_ . output ( ( since_load + run_length ) & 7 , VideoStream : : OutputMode : : Pixels ) ;
2019-10-09 01:18:08 +00:00
}
2019-11-11 02:39:40 +00:00
}
2019-10-09 01:18:08 +00:00
}
2019-11-06 04:02:25 +00:00
// Check for whether line length should have been latched during this run.
2019-12-12 04:22:20 +00:00
if ( x_ < = CYCLE ( 54 ) & & ( x_ + run_length ) > CYCLE ( 54 ) ) line_length_ = horizontal_timings . length ;
2019-11-06 04:02:25 +00:00
2019-11-09 02:42:05 +00:00
// Make a decision about vertical state on cycle 502.
2019-12-12 04:22:20 +00:00
if ( x_ < = CYCLE ( 502 ) & & ( x_ + run_length ) > CYCLE ( 502 ) ) {
2019-11-09 03:18:47 +00:00
next_y_ = y_ + 1 ;
2019-11-09 02:42:05 +00:00
next_vertical_ = vertical_ ;
2019-11-09 03:18:47 +00:00
next_vertical_ . sync_schedule = VerticalState : : SyncSchedule : : None ;
2019-11-09 02:42:05 +00:00
2019-11-20 01:13:47 +00:00
// Use vertical_parameters to get parameters for the current output frequency;
// quick note: things other than the total frame size are counted in terms
// of the line they're evaluated on — i.e. the test is this line, not the next
// one. The total height constraint is obviously whether the next one would be
// too far.
if ( y_ = = vertical_timings . set_enable ) {
2019-11-09 02:42:05 +00:00
next_vertical_ . enable = true ;
2019-11-20 01:13:47 +00:00
} else if ( y_ = = vertical_timings . reset_enable ) {
2019-11-09 02:42:05 +00:00
next_vertical_ . enable = false ;
2020-01-03 01:16:28 +00:00
} else if ( next_y_ = = vertical_timings . height - 2 ) {
2019-12-31 04:01:31 +00:00
next_vertical_ . sync_schedule = VerticalState : : SyncSchedule : : Begin ;
2020-01-03 01:16:28 +00:00
} else if ( next_y_ = = vertical_timings . height ) {
2019-11-09 03:18:47 +00:00
next_y_ = 0 ;
2020-01-03 01:16:28 +00:00
} else if ( y_ = = 0 ) {
2019-12-20 03:20:43 +00:00
next_vertical_ . sync_schedule = VerticalState : : SyncSchedule : : End ;
2019-11-09 02:42:05 +00:00
}
}
2019-11-06 04:02:25 +00:00
// Apply the next event.
2019-11-09 03:18:47 +00:00
x_ + = run_length ;
2019-11-06 04:02:25 +00:00
integer_duration - = run_length ;
2019-12-29 03:50:34 +00:00
// Check horizontal events; the first six are guaranteed to occur separately.
2019-11-09 03:18:47 +00:00
if ( horizontal_timings . reset_blank = = x_ ) horizontal_ . blank = false ;
else if ( horizontal_timings . set_blank = = x_ ) horizontal_ . blank = true ;
else if ( horizontal_timings . reset_enable = = x_ ) horizontal_ . enable = false ;
else if ( horizontal_timings . set_enable = = x_ ) horizontal_ . enable = true ;
2019-12-12 04:22:20 +00:00
else if ( line_length_ - hsync_start = = x_ ) { horizontal_ . sync = true ; horizontal_ . enable = false ; }
else if ( line_length_ - hsync_end = = x_ ) horizontal_ . sync = false ;
2019-12-29 03:50:34 +00:00
// next_load_toggle_ is less predictable; test separately because it may coincide
// with one of the above tests.
if ( next_load_toggle_ = = x_ ) {
2019-12-11 02:24:15 +00:00
next_load_toggle_ = - 1 ;
load_ ^ = true ;
load_base_ = x_ ;
}
2019-11-09 03:18:47 +00:00
// Check vertical events.
2019-11-20 01:13:47 +00:00
if ( vertical_ . sync_schedule ! = VerticalState : : SyncSchedule : : None & & x_ = = vsync_x_position ) {
2019-11-09 03:18:47 +00:00
vertical_ . sync = vertical_ . sync_schedule = = VerticalState : : SyncSchedule : : Begin ;
vertical_ . enable & = ! vertical_ . sync ;
2019-12-29 22:37:09 +00:00
reset_fifo ( ) ; // TODO: remove this, probably, once otherwise stable?
2019-11-09 03:18:47 +00:00
}
2019-11-06 04:02:25 +00:00
// Check whether the terminating event was end-of-line; if so then advance
// the vertical bits of state.
2019-11-09 03:18:47 +00:00
if ( x_ = = line_length_ ) {
x_ = 0 ;
2019-11-09 02:42:05 +00:00
vertical_ = next_vertical_ ;
2019-11-09 03:18:47 +00:00
y_ = next_y_ ;
2019-10-09 01:18:08 +00:00
}
2019-11-18 04:28:00 +00:00
2019-12-29 22:37:09 +00:00
// The address is reloaded during the entire period of vertical sync.
// Cf. http://www.atari-forum.com/viewtopic.php?t=31954&start=50#p324730
if ( vertical_ . sync ) {
current_address_ = base_address_ > > 1 ;
// Consider a shout out to the range observer.
if ( previous_base_address_ ! = base_address_ ) {
previous_base_address_ = base_address_ ;
if ( range_observer_ ) {
range_observer_ - > video_did_change_access_range ( this ) ;
}
}
}
2019-11-18 04:28:00 +00:00
// Chuck any deferred output changes into the queue.
const bool next_display_enable = vertical_ . enable & & horizontal_ . enable ;
if ( display_enable ! = next_display_enable ) {
2019-12-13 03:50:35 +00:00
// Schedule change in outwardly-visible DE line.
2019-11-19 03:12:24 +00:00
add_event ( de_delay_period - integer_duration , next_display_enable ? Event : : Type : : SetDisplayEnable : Event : : Type : : ResetDisplayEnable ) ;
2019-12-13 03:50:35 +00:00
// Schedule change in inwardly-visible effect.
next_load_toggle_ = x_ + 8 ; // 4 cycles = 8 half-cycles
2019-11-18 04:28:00 +00:00
}
2019-12-29 22:37:09 +00:00
if ( horizontal_ . sync ! = hsync ) {
// Schedule change in outwardly-visible hsync line.
add_event ( hsync_delay_period - integer_duration , horizontal_ . sync ? Event : : Type : : SetHsync : Event : : Type : : ResetHsync ) ;
}
2019-12-30 00:03:08 +00:00
if ( vertical_ . sync ! = vsync ) {
// Schedule change in outwardly-visible hsync line.
2019-12-30 03:03:36 +00:00
add_event ( vsync_delay_period - integer_duration , vertical_ . sync ? Event : : Type : : SetVsync : Event : : Type : : ResetVsync ) ;
2019-12-30 00:03:08 +00:00
}
2019-10-09 01:18:08 +00:00
}
2019-11-06 04:02:25 +00:00
}
2019-10-09 01:18:08 +00:00
2019-12-13 04:20:28 +00:00
void Video : : push_latched_data ( ) {
data_latch_read_position_ = ( data_latch_read_position_ + 1 ) & 127 ;
if ( ! ( data_latch_read_position_ & 3 ) ) {
2019-12-28 00:03:10 +00:00
video_stream_ . load (
2019-12-13 04:20:28 +00:00
( uint64_t ( data_latch_ [ ( data_latch_read_position_ - 4 ) & 127 ] ) < < 48 ) |
( uint64_t ( data_latch_ [ ( data_latch_read_position_ - 3 ) & 127 ] ) < < 32 ) |
( uint64_t ( data_latch_ [ ( data_latch_read_position_ - 2 ) & 127 ] ) < < 16 ) |
uint64_t ( data_latch_ [ ( data_latch_read_position_ - 1 ) & 127 ] )
2019-11-11 02:39:40 +00:00
) ;
2019-11-06 04:02:25 +00:00
}
2019-10-05 01:34:15 +00:00
}
2019-10-09 02:29:58 +00:00
2019-12-12 04:22:20 +00:00
void Video : : reset_fifo ( ) {
2019-12-13 04:20:28 +00:00
data_latch_read_position_ = data_latch_position_ = 0 ;
2019-12-12 04:22:20 +00:00
}
2019-11-09 02:25:28 +00:00
bool Video : : hsync ( ) {
2019-12-29 22:37:09 +00:00
return public_state_ . hsync ;
2019-10-09 02:29:58 +00:00
}
bool Video : : vsync ( ) {
2019-12-30 00:03:08 +00:00
return public_state_ . vsync ;
2019-10-09 02:29:58 +00:00
}
bool Video : : display_enabled ( ) {
2019-11-19 03:12:24 +00:00
return public_state_ . display_enable ;
2019-10-09 02:29:58 +00:00
}
HalfCycles Video : : get_next_sequence_point ( ) {
2019-11-06 04:02:25 +00:00
// The next sequence point will be whenever display_enabled, vsync or hsync next changes.
2019-11-09 03:18:47 +00:00
// Sequence of events within a standard line:
2019-11-08 03:00:50 +00:00
//
// 1) blank disabled;
2019-11-09 02:25:28 +00:00
// 2) display enabled;
// 3) display disabled;
2019-11-08 03:00:50 +00:00
// 4) blank enabled;
2019-11-09 02:25:28 +00:00
// 5) sync enabled;
// 6) sync disabled;
// 7) end-of-line, potential vertical event.
2019-11-09 03:18:47 +00:00
//
// If this line has a vertical sync event on it, there will also be an event at cycle 30,
// which will always falls somewhere between (1) and (4) but might or might not be in the
// visible area.
2019-11-08 03:00:50 +00:00
2019-11-06 04:02:25 +00:00
const auto horizontal_timings = horizontal_parameters ( field_frequency_ ) ;
2019-11-08 03:00:50 +00:00
2019-11-19 03:12:24 +00:00
int event_time = line_length_ ; // Worst case: report end of line.
// If any events are pending, give the first of those the chance to be next.
if ( ! pending_events_ . empty ( ) ) {
2019-12-30 02:53:45 +00:00
event_time = std : : min ( event_time , x_ + pending_events_ . front ( ) . delay ) ;
2019-11-19 03:12:24 +00:00
}
// If this is a vertically-enabled line, check for the display enable boundaries, + the standard delay.
2019-11-06 04:02:25 +00:00
if ( vertical_ . enable ) {
2019-11-19 03:12:24 +00:00
if ( x_ < horizontal_timings . set_enable + de_delay_period ) {
event_time = std : : min ( event_time , horizontal_timings . set_enable + de_delay_period ) ;
}
else if ( x_ < horizontal_timings . reset_enable + de_delay_period ) {
event_time = std : : min ( event_time , horizontal_timings . reset_enable + de_delay_period ) ;
2019-11-09 03:18:47 +00:00
}
2019-10-09 02:29:58 +00:00
}
2019-11-19 03:12:24 +00:00
// If a vertical sync event is scheduled, test for that.
2019-11-20 01:13:47 +00:00
if ( vertical_ . sync_schedule ! = VerticalState : : SyncSchedule : : None & & ( x_ < vsync_x_position ) ) {
event_time = std : : min ( event_time , vsync_x_position ) ;
2019-11-19 03:12:24 +00:00
}
2019-12-30 02:53:45 +00:00
// Test for beginning and end of horizontal sync.
if ( x_ < line_length_ - hsync_start + hsync_delay_period ) {
2019-12-29 22:51:50 +00:00
event_time = std : : min ( line_length_ - hsync_start + hsync_delay_period , event_time ) ;
}
2019-12-30 02:53:45 +00:00
/* Hereby assumed: hsync end will be communicated at end of line: */
static_assert ( hsync_end = = hsync_delay_period ) ;
2019-10-09 02:29:58 +00:00
2019-12-30 02:53:45 +00:00
// It wasn't any of those, just supply end of line. That's when the static_assert above assumes a visible hsync transition.
2019-11-19 03:12:24 +00:00
return HalfCycles ( event_time - x_ ) ;
2019-10-09 02:29:58 +00:00
}
2019-10-10 03:01:11 +00:00
// MARK: - IO dispatch
2019-11-02 03:01:06 +00:00
uint16_t Video : : read ( int address ) {
2019-10-11 02:46:58 +00:00
address & = 0x3f ;
switch ( address ) {
2019-10-28 02:39:00 +00:00
default :
break ;
2019-11-02 03:01:06 +00:00
case 0x00 : return uint16_t ( 0xff00 | ( base_address_ > > 16 ) ) ;
case 0x01 : return uint16_t ( 0xff00 | ( base_address_ > > 8 ) ) ;
2019-11-09 02:25:28 +00:00
case 0x02 : return uint16_t ( 0xff00 | ( current_address_ > > 15 ) ) ; // Current address is kept in word precision internally;
case 0x03 : return uint16_t ( 0xff00 | ( current_address_ > > 7 ) ) ; // the shifts here represent a conversion back to
case 0x04 : return uint16_t ( 0xff00 | ( current_address_ < < 1 ) ) ; // byte precision.
2019-11-08 04:11:06 +00:00
2019-11-06 04:02:25 +00:00
case 0x05 : return sync_mode_ | 0xfcff ;
2019-11-02 03:01:06 +00:00
case 0x30 : return video_mode_ | 0xfcff ;
2019-11-08 04:11:06 +00:00
case 0x20 : case 0x21 : case 0x22 : case 0x23 :
case 0x24 : case 0x25 : case 0x26 : case 0x27 :
case 0x28 : case 0x29 : case 0x2a : case 0x2b :
case 0x2c : case 0x2d : case 0x2e : case 0x2f : return raw_palette_ [ address - 0x20 ] ;
2019-10-11 02:46:58 +00:00
}
2019-10-10 03:01:11 +00:00
return 0xff ;
}
2019-10-11 03:29:46 +00:00
void Video : : write ( int address , uint16_t value ) {
2019-10-10 03:01:11 +00:00
address & = 0x3f ;
switch ( address ) {
default : break ;
2019-10-11 02:46:58 +00:00
// Start address.
2019-11-02 03:01:06 +00:00
case 0x00 : base_address_ = ( base_address_ & 0x00ffff ) | ( ( value & 0xff ) < < 16 ) ; break ;
case 0x01 : base_address_ = ( base_address_ & 0xff00ff ) | ( ( value & 0xff ) < < 8 ) ; break ;
2019-10-11 02:46:58 +00:00
2019-11-06 04:02:25 +00:00
// Sync mode and pixel mode.
case 0x05 :
2019-12-14 00:57:54 +00:00
// Writes to sync mode have a one-cycle delay in effect.
2020-01-03 04:36:11 +00:00
deferrer_ . defer ( HalfCycles ( 2 ) , [ = ] {
2019-12-14 00:57:54 +00:00
sync_mode_ = value ;
update_output_mode ( ) ;
2020-01-03 04:36:11 +00:00
} ) ;
2019-11-06 04:02:25 +00:00
break ;
case 0x30 :
video_mode_ = value ;
update_output_mode ( ) ;
break ;
2019-10-28 02:39:00 +00:00
2019-10-10 03:01:11 +00:00
// Palette.
case 0x20 : case 0x21 : case 0x22 : case 0x23 :
case 0x24 : case 0x25 : case 0x26 : case 0x27 :
case 0x28 : case 0x29 : case 0x2a : case 0x2b :
2019-10-11 03:29:46 +00:00
case 0x2c : case 0x2d : case 0x2e : case 0x2f : {
2019-12-28 15:45:10 +00:00
if ( address = = 0x20 ) video_stream_ . will_change_border_colour ( ) ;
2019-11-08 04:11:06 +00:00
raw_palette_ [ address - 0x20 ] = value ;
2019-10-11 03:29:46 +00:00
uint8_t * const entry = reinterpret_cast < uint8_t * > ( & palette_ [ address - 0x20 ] ) ;
entry [ 0 ] = uint8_t ( ( value & 0x700 ) > > 7 ) ;
entry [ 1 ] = uint8_t ( ( value & 0x77 ) < < 1 ) ;
} break ;
2019-10-10 03:01:11 +00:00
}
}
2019-11-06 04:02:25 +00:00
void Video : : update_output_mode ( ) {
2019-12-13 03:50:35 +00:00
const auto old_bpp_ = output_bpp_ ;
2019-12-11 02:24:15 +00:00
2019-11-06 04:02:25 +00:00
// If this is black and white mode, that's that.
switch ( ( video_mode_ > > 8 ) & 3 ) {
case 0 : output_bpp_ = OutputBpp : : Four ; break ;
case 1 : output_bpp_ = OutputBpp : : Two ; break ;
2019-11-19 03:56:40 +00:00
default :
2019-12-13 03:50:35 +00:00
case 2 : output_bpp_ = OutputBpp : : One ; break ;
2019-11-06 04:02:25 +00:00
}
2019-12-13 03:50:35 +00:00
// 1bpp mode ignores the otherwise-programmed frequency.
if ( output_bpp_ = = OutputBpp : : One ) {
field_frequency_ = FieldFrequency : : SeventyTwo ;
} else {
field_frequency_ = ( sync_mode_ & 0x200 ) ? FieldFrequency : : Fifty : FieldFrequency : : Sixty ;
}
if ( output_bpp_ ! = old_bpp_ ) {
// "the 71-Hz-switch does something like a shifter-reset." (and some people use a high-low resolutions switch instead)
2019-12-12 04:22:20 +00:00
reset_fifo ( ) ;
2019-12-28 03:47:34 +00:00
video_stream_ . set_bpp ( output_bpp_ ) ;
2019-12-12 04:22:20 +00:00
}
2019-12-28 04:47:19 +00:00
2020-01-03 04:18:21 +00:00
// const int freqs[] = {50, 60, 72};
// printf("%d, %d -> %d [%d %d]\n", x_ / 2, y_, freqs[int(field_frequency_)], horizontal_.enable, vertical_.enable);
2019-11-06 04:02:25 +00:00
}
2019-11-11 02:39:40 +00:00
// MARK: - The shifter
2019-12-28 03:47:34 +00:00
void Video : : VideoStream : : output ( int duration , OutputMode mode ) {
// If this is a transition from sync to blank, actually transition to colour burst.
if ( output_mode_ = = OutputMode : : Sync & & mode = = OutputMode : : Blank ) {
mode = OutputMode : : ColourBurst ;
2019-11-11 02:39:40 +00:00
}
2019-12-28 03:47:34 +00:00
// If this is seeming a transition from blank to colour burst, obey it only if/when
// sufficient colour burst has been output.
if ( output_mode_ = = OutputMode : : Blank & & mode = = OutputMode : : ColourBurst ) {
if ( duration_ + duration > = 40 ) {
const int overage = duration + duration_ - 40 ;
duration_ = 40 ;
2019-11-11 02:39:40 +00:00
2019-12-28 03:47:34 +00:00
generate ( overage , OutputMode : : ColourBurst , true ) ;
} else {
mode = OutputMode : : ColourBurst ;
2019-12-21 04:49:38 +00:00
}
2019-11-11 02:39:40 +00:00
}
2019-12-28 03:47:34 +00:00
// If this is a transition, or if we're doing pixels, output whatever has been accumulated.
if ( mode ! = output_mode_ | | output_mode_ = = OutputMode : : Pixels ) {
generate ( duration , output_mode_ , mode ! = output_mode_ ) ;
} else {
duration_ + = duration ;
2019-11-11 02:39:40 +00:00
}
2019-12-28 03:47:34 +00:00
// Accumulate time in the current mode.
output_mode_ = mode ;
2019-11-11 02:39:40 +00:00
}
2019-12-28 03:47:34 +00:00
void Video : : VideoStream : : generate ( int duration , OutputMode mode , bool is_terminal ) {
// Three of these are trivial; deal with them upfront. They don't care about the duration of
// whatever is new, just about how much was accumulated prior to now.
if ( mode ! = OutputMode : : Pixels ) {
switch ( mode ) {
default :
case OutputMode : : Sync : crt_ . output_sync ( duration_ ) ; break ;
case OutputMode : : Blank : crt_ . output_blank ( duration_ ) ; break ;
case OutputMode : : ColourBurst : crt_ . output_default_colour_burst ( duration_ ) ; break ;
2019-11-11 02:39:40 +00:00
}
2019-12-28 03:47:34 +00:00
// Reseed duration
duration_ = duration ;
// The shifter should keep running, so throw away the proper amount of content.
shift ( duration_ ) ;
return ;
2019-11-11 02:39:40 +00:00
}
2019-12-28 03:47:34 +00:00
// If the shifter is empty, accumulate in duration_ a promise to draw border later.
2019-11-11 02:39:40 +00:00
if ( ! output_shifter_ ) {
2019-12-28 03:47:34 +00:00
if ( pixel_pointer_ ) {
flush_pixels ( ) ;
}
duration_ + = duration ;
// If this is terminal, we'll need to draw now. But if it isn't, job done.
if ( is_terminal ) {
flush_border ( ) ;
2019-11-13 00:30:28 +00:00
}
2019-12-28 03:47:34 +00:00
2019-11-11 02:39:40 +00:00
return ;
}
2019-12-28 03:47:34 +00:00
// There's definitely some pixels to convey, but perhaps there's some border first?
if ( duration_ ) {
flush_border ( ) ;
}
// Time to do some pixels!
output_pixels ( duration ) ;
// If was terminal, make sure any transient storage is output.
if ( is_terminal ) {
flush_pixels ( ) ;
}
}
2019-12-28 15:45:10 +00:00
void Video : : VideoStream : : will_change_border_colour ( ) {
// Flush the accumulated border if it'd be adversely affected.
if ( duration_ & & output_mode_ = = OutputMode : : Pixels ) {
flush_border ( ) ;
}
}
2019-12-28 03:47:34 +00:00
void Video : : VideoStream : : flush_border ( ) {
// Output colour 0 for the entirety of duration_ (or black, if this is 1bpp mode).
uint16_t * const colour_pointer = reinterpret_cast < uint16_t * > ( crt_ . begin_data ( 1 ) ) ;
2019-12-28 15:45:10 +00:00
if ( colour_pointer ) * colour_pointer = ( bpp_ ! = OutputBpp : : One ) ? palette_ [ 0 ] : 0 ;
2019-12-28 03:47:34 +00:00
crt_ . output_level ( duration_ ) ;
duration_ = 0 ;
}
namespace {
# if TARGET_RT_BIG_ENDIAN
constexpr int upper = 0 ;
# else
constexpr int upper = 1 ;
# endif
}
void Video : : VideoStream : : shift ( int duration ) {
switch ( bpp_ ) {
case OutputBpp : : One :
output_shifter_ < < = ( duration < < 1 ) ;
break ;
case OutputBpp : : Two :
while ( duration - - ) {
shifter_halves_ [ upper ] = ( shifter_halves_ [ upper ] < < 1 ) & 0xfffefffe ;
shifter_halves_ [ upper ] | = ( shifter_halves_ [ upper ^ 1 ] & 0x80008000 ) > > 15 ;
shifter_halves_ [ upper ^ 1 ] = ( shifter_halves_ [ upper ^ 1 ] < < 1 ) & 0xfffefffe ;
}
break ;
case OutputBpp : : Four :
while ( duration ) {
output_shifter_ = ( output_shifter_ < < 1 ) & 0xfffefffefffefffe ;
duration - = 2 ;
}
break ;
2019-11-11 02:39:40 +00:00
}
2019-12-28 03:47:34 +00:00
}
// TODO: turn this into a template on current BPP, perhaps? Would avoid reevaluation of the conditional.
void Video : : VideoStream : : output_pixels ( int duration ) {
constexpr int allocation_size = 352 ; // i.e. 320 plus a spare 32.
2019-11-11 02:39:40 +00:00
2019-12-28 03:47:34 +00:00
// Convert from duration to pixels.
int pixels = duration ;
2019-11-11 02:39:40 +00:00
switch ( bpp_ ) {
2019-12-28 03:47:34 +00:00
case OutputBpp : : One : pixels < < = 1 ; break ;
default : break ;
case OutputBpp : : Four : pixels > > = 1 ; break ;
}
while ( pixels ) {
// If no buffer is currently available, attempt to allocate one.
if ( ! pixel_buffer_ ) {
pixel_buffer_ = reinterpret_cast < uint16_t * > ( crt_ . begin_data ( allocation_size , 2 ) ) ;
// Stop the loop if no buffer is available.
if ( ! pixel_buffer_ ) break ;
}
int pixels_to_draw = std : : min ( allocation_size - pixel_pointer_ , pixels ) ;
pixels - = pixels_to_draw ;
switch ( bpp_ ) {
case OutputBpp : : One :
while ( pixels_to_draw - - ) {
2019-11-11 02:39:40 +00:00
pixel_buffer_ [ pixel_pointer_ ] = ( ( output_shifter_ > > 63 ) & 1 ) * 0xffff ;
output_shifter_ < < = 1 ;
2019-12-28 03:47:34 +00:00
2019-11-11 02:39:40 +00:00
+ + pixel_pointer_ ;
}
2019-12-28 03:47:34 +00:00
break ;
case OutputBpp : : Two :
while ( pixels_to_draw - - ) {
2019-11-11 02:39:40 +00:00
pixel_buffer_ [ pixel_pointer_ ] = palette_ [
( ( output_shifter_ > > 63 ) & 1 ) |
( ( output_shifter_ > > 46 ) & 2 )
] ;
// This ensures that the top two words shift one to the left;
// their least significant bits are fed from the most significant bits
// of the bottom two words, respectively.
shifter_halves_ [ upper ] = ( shifter_halves_ [ upper ] < < 1 ) & 0xfffefffe ;
shifter_halves_ [ upper ] | = ( shifter_halves_ [ upper ^ 1 ] & 0x80008000 ) > > 15 ;
shifter_halves_ [ upper ^ 1 ] = ( shifter_halves_ [ upper ^ 1 ] < < 1 ) & 0xfffefffe ;
+ + pixel_pointer_ ;
}
2019-12-28 03:47:34 +00:00
break ;
case OutputBpp : : Four :
while ( pixels_to_draw - - ) {
2019-11-11 02:39:40 +00:00
pixel_buffer_ [ pixel_pointer_ ] = palette_ [
( ( output_shifter_ > > 63 ) & 1 ) |
( ( output_shifter_ > > 46 ) & 2 ) |
( ( output_shifter_ > > 29 ) & 4 ) |
( ( output_shifter_ > > 12 ) & 8 )
] ;
output_shifter_ = ( output_shifter_ < < 1 ) & 0xfffefffefffefffe ;
2019-12-28 03:47:34 +00:00
2019-11-11 02:39:40 +00:00
+ + pixel_pointer_ ;
}
2019-12-28 03:47:34 +00:00
break ;
}
// Check whether the limit has been reached.
if ( pixel_pointer_ = = allocation_size ) {
flush_pixels ( ) ;
}
2019-11-11 02:39:40 +00:00
}
2019-12-28 03:47:34 +00:00
// If duration remains, that implies no buffer was available, so
// just do the corresponding shifting and provide proper timing to the CRT.
if ( pixels ) {
int leftover_duration = pixels ;
switch ( bpp_ ) {
case OutputBpp : : One : leftover_duration > > = 1 ; break ;
default : break ;
case OutputBpp : : Four : leftover_duration < < = 1 ; break ;
}
shift ( leftover_duration ) ;
crt_ . output_data ( leftover_duration ) ;
}
}
void Video : : VideoStream : : flush_pixels ( ) {
2020-01-03 04:18:21 +00:00
// Flush only if there's something to flush.
if ( pixel_pointer_ ) {
switch ( bpp_ ) {
case OutputBpp : : One : crt_ . output_data ( pixel_pointer_ > > 1 , size_t ( pixel_pointer_ ) ) ; break ;
default : crt_ . output_data ( pixel_pointer_ ) ; break ;
case OutputBpp : : Four : crt_ . output_data ( pixel_pointer_ < < 1 , size_t ( pixel_pointer_ ) ) ; break ;
}
2019-12-28 03:47:34 +00:00
}
pixel_pointer_ = 0 ;
pixel_buffer_ = nullptr ;
}
void Video : : VideoStream : : set_bpp ( OutputBpp bpp ) {
2020-01-03 04:18:21 +00:00
// Terminate the allocated block of memory (if any).
flush_pixels ( ) ;
// Reset the shifter.
2019-12-28 03:47:34 +00:00
// TODO: is flushing like this correct?
output_shifter_ = 0 ;
2020-01-03 04:18:21 +00:00
// Store the new BPP.
2019-12-28 03:47:34 +00:00
bpp_ = bpp ;
2019-11-11 02:39:40 +00:00
}
2019-12-28 00:03:10 +00:00
void Video : : VideoStream : : load ( uint64_t value ) {
2019-11-11 02:39:40 +00:00
output_shifter_ = value ;
}
2019-12-09 01:20:13 +00:00
// MARK: - Range observer.
Video : : Range Video : : get_memory_access_range ( ) {
Range range ;
range . low_address = uint32_t ( previous_base_address_ ) ;
range . high_address = range . low_address + 56994 ;
// 56994 is pessimistic but unscientific, being derived from the resolution of the largest
// fullscreen demo I could quickly find documentation of. TODO: calculate real number.
return range ;
}
void Video : : set_range_observer ( RangeObserver * observer ) {
range_observer_ = observer ;
observer - > video_did_change_access_range ( this ) ;
}