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
2020-01-28 04:40:01 +00:00
# define CYCLE(x) ((x) * 2)
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 } ,
2020-02-02 22:26:39 +00:00
{ 34 , 434 , 500 } // Guesswork: (i) nobody ever recommends 72Hz mode for opening the top border, so it's likely to be the same as another mode; (ii) being the same as PAL feels too late.
2019-11-06 04:02:25 +00:00
} ;
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-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
2020-01-28 04:00:30 +00:00
const int vertical_decision ;
2020-01-28 04:40:01 +00:00
LineLength length ;
2019-11-09 01:46:24 +00:00
} horizontal_params [ 3 ] = {
2020-01-28 04:40:01 +00:00
{ CYCLE ( 56 ) , CYCLE ( 376 ) , CYCLE ( 450 ) , CYCLE ( 28 ) , CYCLE ( 502 ) , { CYCLE ( 512 ) , CYCLE ( 464 ) , CYCLE ( 504 ) } } ,
{ CYCLE ( 52 ) , CYCLE ( 372 ) , CYCLE ( 450 ) , CYCLE ( 24 ) , CYCLE ( 502 ) , { CYCLE ( 508 ) , CYCLE ( 460 ) , CYCLE ( 500 ) } } ,
{ CYCLE ( 4 ) , CYCLE ( 164 ) , CYCLE ( 999 ) , CYCLE ( 999 ) , CYCLE ( 214 ) , { CYCLE ( 224 ) , CYCLE ( 194 ) , CYCLE ( 212 ) } }
// 72Hz mode doesn't set or reset blank.
2019-10-09 01:18:08 +00:00
} ;
2020-01-28 04:00:30 +00:00
// Re: 'vertical_decision':
// This is cycle 502 if in 50 or 60 Hz mode; in 70 Hz mode I've put it on cycle 214
// in order to be analogous to 50 and 60 Hz mode. I have no idea where it should
// actually go.
2020-01-28 04:40:01 +00:00
//
// Ditto the horizontal sync timings for 72Hz are plucked out of thin air.
2020-01-28 04:00:30 +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 ) ;
2020-01-28 04:41:08 +00:00
assert ( horizontal . set_blank + 50 < horizontal . length . length ) ;
2019-12-11 03:17:57 +00:00
} else {
assert ( horizontal . set_enable < horizontal . reset_enable ) ;
2020-01-28 04:41:08 +00:00
assert ( horizontal . set_enable + 50 < horizontal . length . length ) ;
2019-12-11 03:17:57 +00:00
}
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.
2020-01-29 01:22:37 +00:00
const int line_length_latch_position = CYCLE ( 54 ) ;
2019-12-30 00:03:08 +00:00
2020-01-28 04:40:01 +00:00
const int hsync_delay_period = CYCLE ( 8 ) ; // Signal hsync at the end of the line.
2019-12-30 00:03:08 +00:00
const int vsync_delay_period = hsync_delay_period ; // Signal vsync with the same delay as hsync.
2019-11-20 01:13:47 +00:00
2020-01-30 03:50:22 +00:00
const int load_delay_period = CYCLE ( 4 ) ; // Amount of time after DE that observed DE changes. NB: HACK HERE. This currently incorporates the MFP recognition delay. MUST FIX.
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 ( ) :
2020-02-02 22:26:39 +00:00
crt_ ( 2048 , 2 , Outputs : : Display : : Type : : PAL50 , Outputs : : Display : : InputDataType : : Red4Green4Blue4 ) ,
// crt_(896, 1, 500, 5, 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-02-02 22:26:39 +00:00
crt_ . set_visible_area ( crt_ . get_rect_for_area ( 33 , 260 , 440 , 1700 , 4.0f / 3.0f ) ) ;
2019-10-05 01:34:15 +00:00
}
2022-08-10 20:36:11 +00:00
void Video : : set_ram ( uint16_t * ram , size_t size ) {
2019-10-11 02:46:58 +00:00
ram_ = ram ;
2022-08-10 20:41:45 +00:00
ram_mask_ = int ( size - 1 ) ;
2019-10-11 02:46:58 +00:00
}
2019-10-05 01:34:15 +00:00
void Video : : set_scan_target ( Outputs : : Display : : ScanTarget * scan_target ) {
crt_ . set_scan_target ( scan_target ) ;
}
2020-01-22 03:28:25 +00:00
Outputs : : Display : : ScanStatus Video : : get_scaled_scan_status ( ) const {
2020-01-28 04:08:28 +00:00
return crt_ . get_scaled_scan_status ( ) / 4.0f ;
2020-01-21 02:45:10 +00:00
}
2019-12-21 01:49:14 +00:00
void Video : : set_display_type ( Outputs : : Display : : DisplayType display_type ) {
crt_ . set_display_type ( display_type ) ;
}
2020-05-21 03:34:26 +00:00
Outputs : : Display : : DisplayType Video : : get_display_type ( ) const {
2020-03-18 03:52:55 +00:00
return crt_ . get_display_type ( ) ;
}
2019-10-05 01:34:15 +00:00
void Video : : run_for ( HalfCycles duration ) {
2019-11-06 04:02:25 +00:00
int integer_duration = int ( duration . as_integral ( ) ) ;
2020-01-30 03:46:08 +00:00
assert ( integer_duration > = 0 ) ;
2019-11-18 04:28:00 +00:00
2019-10-09 01:18:08 +00:00
while ( integer_duration ) {
2020-01-30 03:46:08 +00:00
const auto horizontal_timings = horizontal_parameters ( field_frequency_ ) ;
const auto vertical_timings = vertical_parameters ( field_frequency_ ) ;
// Determine time to next event; this'll either be one of the ones informally scheduled in here,
// or something from the deferral queue.
2019-11-06 04:02:25 +00:00
// Seed next event to end of line.
2020-01-28 04:40:01 +00:00
int next_event = line_length_ . length ;
2019-11-06 04:02:25 +00:00
2020-01-30 03:46:08 +00:00
const int next_deferred_event = deferrer_ . time_until_next_action ( ) . as < int > ( ) ;
if ( next_deferred_event > = 0 )
next_event = std : : min ( next_event , next_deferred_event + x_ ) ;
2019-11-06 04:02:25 +00:00
// 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 ) ;
2023-05-12 18:14:45 +00:00
if ( horizontal_timings . set_enable > x_ ) next_event = std : : min ( next_event , horizontal_timings . set_enable ) ;
2019-11-06 04:02:25 +00:00
// Check for events that are relative to existing latched state.
2020-01-28 04:40:01 +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
2020-01-30 03:46:08 +00:00
assert ( run_length > 0 ) ;
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.
2020-02-09 22:04:49 +00:00
int start_column = ( since_load - 1 ) > > 3 ;
const int end_column = ( since_load + run_length - 1 ) > > 3 ;
2019-12-13 04:20:28 +00:00
while ( start_column ! = end_column ) {
2022-08-10 20:36:11 +00:00
data_latch_ [ data_latch_position_ ] = ram_ [ current_address_ & ram_mask_ ] ;
2019-12-13 04:20:28 +00:00
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 {
2020-02-02 22:26:39 +00:00
const int start = x_ - load_base_ ;
const int end = start + run_length ;
2019-12-11 02:24:15 +00:00
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.
2020-02-02 22:26:39 +00:00
int start_column = start > > 3 ;
const int end_column = end > > 3 ;
const int start_offset = start & 7 ;
const int end_offset = end & 7 ;
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 ) {
2020-02-02 22:26:39 +00:00
if ( ! start_offset ) {
push_latched_data ( ) ;
}
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.
2020-02-02 22:26:39 +00:00
if ( start_offset ) {
2019-11-11 02:39:40 +00:00
// If at least one column boundary is crossed, complete this column.
2020-02-02 22:26:39 +00:00
video_stream_ . output ( 8 - start_offset , VideoStream : : OutputMode : : Pixels ) ;
2019-11-11 02:39:40 +00:00
+ + start_column ; // This starts a new column, so latch a new word.
}
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-13 04:20:28 +00:00
push_latched_data ( ) ;
2020-02-02 22:26:39 +00:00
video_stream_ . output ( 8 , VideoStream : : OutputMode : : Pixels ) ;
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.
2020-02-02 22:26:39 +00:00
if ( end_offset ) {
push_latched_data ( ) ;
video_stream_ . output ( end_offset , 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.
2020-01-29 01:22:37 +00:00
if ( x_ < line_length_latch_position & & ( x_ + run_length ) > = line_length_latch_position ) {
line_length_ = horizontal_timings . length ;
}
2019-11-06 04:02:25 +00:00
2020-01-28 04:00:30 +00:00
// Make a decision about vertical state on the appropriate cycle.
2020-01-29 01:22:37 +00:00
if ( x_ < horizontal_timings . vertical_decision & & ( x_ + run_length ) > = horizontal_timings . vertical_decision ) {
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 ;
2020-01-30 03:46:08 +00:00
assert ( integer_duration > = run_length ) ;
2019-11-06 04:02:25 +00:00
integer_duration - = run_length ;
2020-01-30 03:46:08 +00:00
deferrer_ . advance ( HalfCycles ( run_length ) ) ;
2019-11-06 04:02:25 +00:00
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 ;
2023-05-12 18:14:45 +00:00
else if ( horizontal_timings . set_enable = = x_ ) horizontal_ . enable = true ;
2020-01-28 04:40:01 +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
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.
2020-01-28 04:40:01 +00:00
if ( x_ = = line_length_ . length ) {
2019-11-09 03:18:47 +00:00
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 ) {
2020-01-30 03:50:22 +00:00
// Schedule change in load line.
deferrer_ . defer ( load_delay_period , [ this , next_display_enable ] {
this - > load_ = next_display_enable ;
this - > load_base_ = this - > x_ ;
} ) ;
2019-12-13 03:50:35 +00:00
// Schedule change in outwardly-visible DE line.
2020-01-30 03:46:08 +00:00
deferrer_ . defer ( de_delay_period , [ this , next_display_enable ] {
this - > public_state_ . display_enable = next_display_enable ;
} ) ;
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.
2020-01-30 03:46:08 +00:00
deferrer_ . defer ( hsync_delay_period , [ this , next_horizontal_sync = horizontal_ . sync ] {
this - > public_state_ . hsync = next_horizontal_sync ;
} ) ;
2019-12-29 22:37:09 +00:00
}
2019-12-30 00:03:08 +00:00
if ( vertical_ . sync ! = vsync ) {
// Schedule change in outwardly-visible hsync line.
2020-01-30 03:46:08 +00:00
deferrer_ . defer ( vsync_delay_period , [ this , next_vertical_sync = vertical_ . sync ] {
this - > public_state_ . vsync = next_vertical_sync ;
} ) ;
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
}
2023-09-10 22:00:49 +00:00
HalfCycles Video : : 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
2020-01-28 04:40:01 +00:00
int event_time = line_length_ . length ; // Worst case: report end of line.
2019-11-19 03:12:24 +00:00
// If any events are pending, give the first of those the chance to be next.
2020-01-30 03:18:41 +00:00
const auto next_deferred_item = deferrer_ . time_until_next_action ( ) ;
if ( next_deferred_item ! = HalfCycles ( - 1 ) ) {
2020-01-30 03:46:08 +00:00
event_time = std : : min ( event_time , x_ + next_deferred_item . as < int > ( ) ) ;
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.
2020-01-28 04:40:01 +00:00
if ( x_ < line_length_ . hsync_start + hsync_delay_period ) {
event_time = std : : min ( line_length_ . hsync_start + hsync_delay_period , event_time ) ;
2019-12-29 22:51:50 +00:00
}
2020-01-29 01:38:20 +00:00
if ( x_ < line_length_ . hsync_end + hsync_delay_period ) {
event_time = std : : min ( line_length_ . hsync_end + hsync_delay_period , event_time ) ;
}
2019-10-09 02:29:58 +00:00
2020-01-29 01:22:37 +00:00
// Also factor in the line length latching time.
if ( x_ < line_length_latch_position ) {
event_time = std : : min ( line_length_latch_position , event_time ) ;
}
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-02-15 04:39:08 +00:00
deferrer_ . defer ( HalfCycles ( 2 ) , [ this , value ] {
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 :
2020-01-28 04:08:28 +00:00
case OutputMode : : Sync : crt_ . output_sync ( duration_ * 2 ) ; break ;
case OutputMode : : Blank : crt_ . output_blank ( duration_ * 2 ) ; break ;
case OutputMode : : ColourBurst : crt_ . output_default_colour_burst ( duration_ * 2 ) ; break ;
2019-11-11 02:39:40 +00:00
}
2019-12-28 03:47:34 +00:00
2020-02-02 22:26:39 +00:00
// Reseed duration.
2019-12-28 03:47:34 +00:00
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).
2023-12-07 16:04:18 +00:00
crt_ . output_level < uint16_t > ( duration_ * 2 , ( bpp_ ! = OutputBpp : : One ) ? palette_ [ 0 ] : 0 ) ;
2019-12-28 03:47:34 +00:00
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.
2020-02-02 22:26:39 +00:00
if ( pixel_pointer_ > = allocation_size - 32 ) {
2019-12-28 03:47:34 +00:00
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_ ) {
2023-05-12 18:14:45 +00:00
default : leftover_duration > > = 1 ; break ;
2020-02-02 22:26:39 +00:00
case OutputBpp : : Two : break ;
case OutputBpp : : Four : leftover_duration < < = 1 ; break ;
2019-12-28 03:47:34 +00:00
}
shift ( leftover_duration ) ;
2020-01-28 04:08:28 +00:00
crt_ . output_data ( leftover_duration * 2 ) ;
2019-12-28 03:47:34 +00:00
}
}
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_ ) {
2023-05-12 18:14:45 +00:00
case OutputBpp : : One : crt_ . output_data ( pixel_pointer_ ) ; break ;
2020-02-02 22:26:39 +00:00
default : crt_ . output_data ( pixel_pointer_ < < 1 , size_t ( pixel_pointer_ ) ) ; break ;
case OutputBpp : : Four : crt_ . output_data ( pixel_pointer_ < < 2 , size_t ( pixel_pointer_ ) ) ; break ;
2020-01-03 04:18:21 +00:00
}
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 ) {
2020-01-28 04:00:30 +00:00
// In 1bpp mode, a 0 bit is white and a 1 bit is black.
// Invert the input so that the 'just output the border colour
// when the shifter is empty' optimisation works.
if ( bpp_ = = OutputBpp : : One )
output_shifter_ = ~ value ;
else
output_shifter_ = value ;
2019-11-11 02:39:40 +00:00
}
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 ) ;
}