2019-05-04 03:25:42 +00:00
|
|
|
//
|
|
|
|
// Video.cpp
|
|
|
|
// Clock Signal
|
|
|
|
//
|
|
|
|
// Created by Thomas Harte on 03/05/2019.
|
|
|
|
// Copyright © 2019 Thomas Harte. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
#include "Video.hpp"
|
|
|
|
|
2019-05-05 02:27:58 +00:00
|
|
|
#include <algorithm>
|
|
|
|
|
2019-05-04 03:25:42 +00:00
|
|
|
using namespace Apple::Macintosh;
|
|
|
|
|
2019-05-05 02:27:58 +00:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
const HalfCycles line_length(704);
|
|
|
|
const int number_of_lines = 370;
|
|
|
|
const HalfCycles frame_length(line_length * HalfCycles(number_of_lines));
|
2019-05-08 20:54:19 +00:00
|
|
|
const int sync_start = 36;
|
|
|
|
const int sync_end = 38;
|
2019-05-05 02:27:58 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-05-04 03:25:42 +00:00
|
|
|
// Re: CRT timings, see the Apple Guide to the Macintosh Hardware Family,
|
|
|
|
// bottom of page 400:
|
|
|
|
//
|
|
|
|
// "For each scan line, 512 pixels are drawn on the screen ...
|
|
|
|
// The horizontal blanking interval takes the time of an additional 192 pixels"
|
|
|
|
//
|
|
|
|
// And, at the top of 401:
|
|
|
|
//
|
|
|
|
// "The visible portion of a full-screen display consists of 342 horizontal scan lines...
|
|
|
|
// During the vertical blanking interval, the turned-off beam ... traces out an additional 28 scan lines,"
|
|
|
|
//
|
2019-06-01 23:31:32 +00:00
|
|
|
Video::Video(uint16_t *ram, DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator) :
|
2019-06-01 19:03:15 +00:00
|
|
|
audio_(audio),
|
2019-06-01 23:31:32 +00:00
|
|
|
drive_speed_accumulator_(drive_speed_accumulator),
|
2019-05-05 02:27:58 +00:00
|
|
|
crt_(704, 1, 370, Outputs::Display::ColourSpace::YIQ, 1, 1, 6, false, Outputs::Display::InputDataType::Luminance1),
|
|
|
|
ram_(ram) {
|
|
|
|
|
2019-05-06 03:22:05 +00:00
|
|
|
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
2019-05-05 02:27:58 +00:00
|
|
|
crt_.set_visible_area(Outputs::Display::Rect(0.02f, 0.025f, 0.94f, 0.94f));
|
2019-05-04 03:25:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
|
|
|
crt_.set_scan_target(scan_target);
|
|
|
|
}
|
2019-05-04 18:23:37 +00:00
|
|
|
|
2019-05-05 02:27:58 +00:00
|
|
|
void Video::run_for(HalfCycles duration) {
|
|
|
|
|
|
|
|
// The number of HalfCycles is literally the number of pixel clocks to move through,
|
|
|
|
// since pixel output occurs at twice the processor clock. So divide by 16 to get
|
|
|
|
// the number of fetches.
|
|
|
|
while(duration > HalfCycles(0)) {
|
|
|
|
auto cycles_left_in_line = std::min(line_length - frame_position_%line_length, duration);
|
|
|
|
|
|
|
|
const int line = (frame_position_ / line_length).as_int();
|
|
|
|
const auto pixel_start = frame_position_ % line_length;
|
|
|
|
|
|
|
|
// Line timing, entirely invented as I can find exactly zero words of documentation:
|
|
|
|
//
|
|
|
|
// First 342 lines:
|
|
|
|
//
|
|
|
|
// First 32 words = pixels;
|
|
|
|
// next 5 words = right border;
|
|
|
|
// next 2 words = sync level;
|
|
|
|
// final 5 words = left border.
|
|
|
|
//
|
|
|
|
// Then 12 lines of border, 3 of sync, 11 more of border.
|
|
|
|
|
|
|
|
const int first_word = pixel_start.as_int() >> 4;
|
|
|
|
const int final_word = (pixel_start + cycles_left_in_line).as_int() >> 4;
|
|
|
|
|
|
|
|
if(first_word != final_word) {
|
|
|
|
if(line < 342) {
|
|
|
|
// If there are any pixels left to output, do so.
|
|
|
|
if(first_word < 32) {
|
|
|
|
const int final_pixel_word = std::min(final_word, 32);
|
|
|
|
|
|
|
|
if(!first_word) {
|
|
|
|
pixel_buffer_ = crt_.begin_data(512);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(pixel_buffer_) {
|
|
|
|
for(int c = first_word; c < final_pixel_word; ++c) {
|
2019-05-06 01:55:34 +00:00
|
|
|
uint16_t pixels = ram_[video_address_] ^ 0xffff;
|
2019-05-05 02:27:58 +00:00
|
|
|
++video_address_;
|
|
|
|
|
2019-05-06 01:55:34 +00:00
|
|
|
pixel_buffer_[15] = pixels & 0x01;
|
|
|
|
pixel_buffer_[14] = pixels & 0x02;
|
|
|
|
pixel_buffer_[13] = pixels & 0x04;
|
|
|
|
pixel_buffer_[12] = pixels & 0x08;
|
|
|
|
pixel_buffer_[11] = pixels & 0x10;
|
|
|
|
pixel_buffer_[10] = pixels & 0x20;
|
|
|
|
pixel_buffer_[9] = pixels & 0x40;
|
|
|
|
pixel_buffer_[8] = pixels & 0x80;
|
2019-05-05 02:27:58 +00:00
|
|
|
|
|
|
|
pixels >>= 8;
|
2019-05-06 01:55:34 +00:00
|
|
|
pixel_buffer_[7] = pixels & 0x01;
|
|
|
|
pixel_buffer_[6] = pixels & 0x02;
|
|
|
|
pixel_buffer_[5] = pixels & 0x04;
|
|
|
|
pixel_buffer_[4] = pixels & 0x08;
|
|
|
|
pixel_buffer_[3] = pixels & 0x10;
|
|
|
|
pixel_buffer_[2] = pixels & 0x20;
|
|
|
|
pixel_buffer_[1] = pixels & 0x40;
|
|
|
|
pixel_buffer_[0] = pixels & 0x80;
|
2019-05-05 02:27:58 +00:00
|
|
|
|
|
|
|
pixel_buffer_ += 16;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(final_pixel_word == 32) {
|
|
|
|
crt_.output_data(512);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(first_word < sync_start && final_word >= sync_start) crt_.output_blank((sync_start - 32) * 16);
|
|
|
|
if(first_word < sync_end && final_word >= sync_end) crt_.output_sync((sync_end - sync_start) * 16);
|
|
|
|
if(final_word == 44) crt_.output_blank((44 - sync_end) * 16);
|
|
|
|
} else if(line >= 353 && line < 356) {
|
|
|
|
/* Output a sync line. */
|
|
|
|
if(final_word == 44) {
|
|
|
|
crt_.output_sync(sync_start * 16);
|
|
|
|
crt_.output_blank((sync_end - sync_start) * 16);
|
|
|
|
crt_.output_sync((44 - sync_end) * 16);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
/* Output a blank line. */
|
|
|
|
if(final_word == 44) {
|
|
|
|
crt_.output_blank(sync_start * 16);
|
|
|
|
crt_.output_sync((sync_end - sync_start) * 16);
|
|
|
|
crt_.output_blank((44 - sync_end) * 16);
|
|
|
|
}
|
|
|
|
}
|
2019-06-01 19:03:15 +00:00
|
|
|
|
|
|
|
// Audio and disk fetches occur "just before video data".
|
|
|
|
if(final_word == 44) {
|
|
|
|
const uint16_t audio_word = ram_[audio_address_];
|
|
|
|
++audio_address_;
|
|
|
|
audio_.audio.post_sample(audio_word >> 8);
|
2019-06-01 23:31:32 +00:00
|
|
|
drive_speed_accumulator_.post_sample(audio_word & 0xff);
|
2019-06-01 19:03:15 +00:00
|
|
|
}
|
2019-05-05 02:27:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
duration -= cycles_left_in_line;
|
|
|
|
frame_position_ = frame_position_ + cycles_left_in_line;
|
|
|
|
if(frame_position_ == frame_length) {
|
|
|
|
frame_position_ = HalfCycles(0);
|
2019-05-06 03:05:24 +00:00
|
|
|
/*
|
|
|
|
Video: $1A700 and the alternate buffer starts at $12700; for a 512K Macintosh, add $60000 to these numbers.
|
|
|
|
*/
|
2019-06-03 18:50:36 +00:00
|
|
|
video_address_ = (use_alternate_screen_buffer_ ? (0xffff2700 >> 1) : (0xffffa700 >> 1)) & ram_mask_;
|
2019-06-01 19:03:15 +00:00
|
|
|
|
|
|
|
/*
|
|
|
|
"The main sound buffer is at $1FD00 in a 128K Macintosh, and the alternate buffer is at $1A100;
|
|
|
|
for a 512K Macintosh, add $60000 to these values."
|
|
|
|
*/
|
2019-06-03 18:50:36 +00:00
|
|
|
audio_address_ = (use_alternate_audio_buffer_ ? (0xffffa100 >> 1) : (0xfffffd00 >> 1)) & ram_mask_;
|
2019-05-05 02:27:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-08 20:54:19 +00:00
|
|
|
bool Video::vsync() {
|
|
|
|
const int line = (frame_position_ / line_length).as_int();
|
|
|
|
return line >= 353 && line < 356;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Video::is_outputting() {
|
|
|
|
const int column = (frame_position_ % line_length).as_int() >> 4;
|
|
|
|
const int line = (frame_position_ / line_length).as_int();
|
|
|
|
return line < 342 && column < 32;
|
|
|
|
}
|
|
|
|
|
2019-06-01 19:03:15 +00:00
|
|
|
void Video::set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer) {
|
2019-05-06 03:05:24 +00:00
|
|
|
use_alternate_screen_buffer_ = use_alternate_screen_buffer;
|
2019-06-01 19:03:15 +00:00
|
|
|
use_alternate_audio_buffer_ = use_alternate_audio_buffer;
|
2019-05-06 03:05:24 +00:00
|
|
|
}
|
2019-06-03 18:50:36 +00:00
|
|
|
|
|
|
|
void Video::set_ram_mask(uint32_t mask) {
|
|
|
|
ram_mask_ = mask;
|
|
|
|
}
|