mirror of
https://github.com/TomHarte/CLK.git
synced 2025-04-09 00:37:27 +00:00
Merge branch 'master' into Z80Lines
This commit is contained in:
commit
627b96f73c
@ -10,7 +10,9 @@
|
||||
#define ClockReceiver_hpp
|
||||
|
||||
#include "ForceInline.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
/*
|
||||
Informal pattern for all classes that run from a clock cycle:
|
||||
@ -176,6 +178,9 @@ class Cycles: public WrappedInt<Cycles> {
|
||||
public:
|
||||
forceinline constexpr Cycles(IntType l) noexcept : WrappedInt<Cycles>(l) {}
|
||||
forceinline constexpr Cycles() noexcept : WrappedInt<Cycles>() {}
|
||||
forceinline static constexpr Cycles max() {
|
||||
return Cycles(std::numeric_limits<IntType>::max());
|
||||
}
|
||||
|
||||
private:
|
||||
friend WrappedInt;
|
||||
@ -195,6 +200,9 @@ class HalfCycles: public WrappedInt<HalfCycles> {
|
||||
public:
|
||||
forceinline constexpr HalfCycles(IntType l) noexcept : WrappedInt<HalfCycles>(l) {}
|
||||
forceinline constexpr HalfCycles() noexcept : WrappedInt<HalfCycles>() {}
|
||||
forceinline static constexpr HalfCycles max() {
|
||||
return HalfCycles(std::numeric_limits<IntType>::max());
|
||||
}
|
||||
|
||||
forceinline constexpr HalfCycles(const Cycles &cycles) noexcept : WrappedInt<HalfCycles>(cycles.as_integral() * 2) {}
|
||||
|
||||
|
@ -10,6 +10,7 @@
|
||||
#define JustInTime_h
|
||||
|
||||
#include "../Concurrency/AsyncTaskQueue.hpp"
|
||||
#include "ClockingHintSource.hpp"
|
||||
#include "ForceInline.hpp"
|
||||
|
||||
/*!
|
||||
@ -24,29 +25,69 @@
|
||||
|
||||
If the held object implements get_next_sequence_point() then it'll be used to flush implicitly
|
||||
as and when sequence points are hit. Callers can use will_flush() to predict these.
|
||||
|
||||
If the held object is a subclass of ClockingHint::Source, this template will register as an
|
||||
observer and potentially stop clocking or stop delaying clocking until just-in-time references
|
||||
as directed.
|
||||
|
||||
TODO: incorporate and codify AsyncJustInTimeActor.
|
||||
*/
|
||||
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class JustInTimeActor {
|
||||
template <class T, class LocalTimeScale = HalfCycles, int multiplier = 1, int divider = 1> class JustInTimeActor:
|
||||
public ClockingHint::Observer {
|
||||
private:
|
||||
/*!
|
||||
A std::unique_ptr deleter which causes an update_sequence_point to occur on the actor supplied
|
||||
to it at construction if it implements get_next_sequence_point(). Otherwise destruction is a no-op.
|
||||
|
||||
**Does not delete the object.**
|
||||
|
||||
This is used by the -> operators below, which provide a unique pointer to the enclosed object and
|
||||
update their sequence points upon its destruction — i.e. after the caller has made whatever call
|
||||
or calls as were relevant to the enclosed object.
|
||||
*/
|
||||
class SequencePointAwareDeleter {
|
||||
public:
|
||||
explicit SequencePointAwareDeleter(JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *actor) : actor_(actor) {}
|
||||
explicit SequencePointAwareDeleter(JustInTimeActor<T, LocalTimeScale, multiplier, divider> *actor) noexcept
|
||||
: actor_(actor) {}
|
||||
|
||||
void operator ()(const T *const) const {
|
||||
forceinline void operator ()(const T *const) const {
|
||||
if constexpr (has_sequence_points<T>::value) {
|
||||
actor_->update_sequence_point();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *const actor_;
|
||||
JustInTimeActor<T, LocalTimeScale, multiplier, divider> *const actor_;
|
||||
};
|
||||
|
||||
// This block of SFINAE determines whether objects of type T accepts Cycles or HalfCycles.
|
||||
using HalfRunFor = void (T::*const)(HalfCycles);
|
||||
static uint8_t half_sig(...);
|
||||
static uint16_t half_sig(HalfRunFor);
|
||||
using TargetTimeScale =
|
||||
std::conditional_t<
|
||||
sizeof(half_sig(&T::run_for)) == sizeof(uint16_t),
|
||||
HalfCycles,
|
||||
Cycles>;
|
||||
|
||||
public:
|
||||
/// Constructs a new JustInTimeActor using the same construction arguments as the included object.
|
||||
template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
|
||||
template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {
|
||||
if constexpr (std::is_base_of<ClockingHint::Source, T>::value) {
|
||||
object_.set_clocking_hint_observer(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds time to the actor.
|
||||
forceinline void operator += (LocalTimeScale rhs) {
|
||||
///
|
||||
/// @returns @c true if adding time caused a flush; @c false otherwise.
|
||||
forceinline bool operator += (LocalTimeScale rhs) {
|
||||
if constexpr (std::is_base_of<ClockingHint::Source, T>::value) {
|
||||
if(clocking_preference_ == ClockingHint::Preference::None) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (multiplier != 1) {
|
||||
time_since_update_ += rhs * multiplier;
|
||||
} else {
|
||||
@ -54,20 +95,31 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
}
|
||||
is_flushed_ = false;
|
||||
|
||||
if constexpr (std::is_base_of<ClockingHint::Source, T>::value) {
|
||||
if (clocking_preference_ == ClockingHint::Preference::RealTime) {
|
||||
flush();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (has_sequence_points<T>::value) {
|
||||
time_until_event_ -= rhs;
|
||||
if(time_until_event_ <= LocalTimeScale(0)) {
|
||||
time_overrun_ = time_until_event_;
|
||||
flush();
|
||||
update_sequence_point();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Flushes all accumulated time and returns a pointer to the included object.
|
||||
///
|
||||
/// If this object provides sequence points, checks for changes to the next
|
||||
/// sequence point upon deletion of the pointer.
|
||||
forceinline auto operator->() {
|
||||
[[nodiscard]] forceinline auto operator->() {
|
||||
flush();
|
||||
return std::unique_ptr<T, SequencePointAwareDeleter>(&object_, SequencePointAwareDeleter(this));
|
||||
}
|
||||
@ -75,19 +127,24 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
/// Acts exactly as per the standard ->, but preserves constness.
|
||||
///
|
||||
/// Despite being const, this will flush the object and, if relevant, update the next sequence point.
|
||||
forceinline auto operator -> () const {
|
||||
auto non_const_this = const_cast<JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *>(this);
|
||||
[[nodiscard]] forceinline auto operator -> () const {
|
||||
auto non_const_this = const_cast<JustInTimeActor<T, LocalTimeScale, multiplier, divider> *>(this);
|
||||
non_const_this->flush();
|
||||
return std::unique_ptr<const T, SequencePointAwareDeleter>(&object_, SequencePointAwareDeleter(non_const_this));
|
||||
}
|
||||
|
||||
/// @returns a pointer to the included object, without flushing time.
|
||||
forceinline T *last_valid() {
|
||||
[[nodiscard]] forceinline T *last_valid() {
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// @returns a const pointer to the included object, without flushing time.
|
||||
[[nodiscard]] forceinline const T *last_valid() const {
|
||||
return &object_;
|
||||
}
|
||||
|
||||
/// @returns the amount of time since the object was last flushed, in the target time scale.
|
||||
forceinline TargetTimeScale time_since_flush() const {
|
||||
[[nodiscard]] forceinline TargetTimeScale time_since_flush() const {
|
||||
// TODO: does this handle conversions properly where TargetTimeScale != LocalTimeScale?
|
||||
if constexpr (divider == 1) {
|
||||
return time_since_update_;
|
||||
@ -113,20 +170,26 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
}
|
||||
|
||||
/// Indicates whether a flush has occurred since the last call to did_flush().
|
||||
forceinline bool did_flush() {
|
||||
[[nodiscard]] forceinline bool did_flush() {
|
||||
const bool did_flush = did_flush_;
|
||||
did_flush_ = false;
|
||||
return did_flush;
|
||||
}
|
||||
|
||||
/// @returns a number in the range [-max, 0] indicating the offset of the most recent sequence
|
||||
/// point from the final time at the end of the += that triggered the sequence point.
|
||||
[[nodiscard]] forceinline LocalTimeScale last_sequence_point_overrun() {
|
||||
return time_overrun_;
|
||||
}
|
||||
|
||||
/// @returns the number of cycles until the next sequence-point-based flush, if the embedded object
|
||||
/// supports sequence points; @c LocalTimeScale() otherwise.
|
||||
LocalTimeScale cycles_until_implicit_flush() const {
|
||||
[[nodiscard]] LocalTimeScale cycles_until_implicit_flush() const {
|
||||
return time_until_event_;
|
||||
}
|
||||
|
||||
/// Indicates whether a sequence-point-caused flush will occur if the specified period is added.
|
||||
forceinline bool will_flush(LocalTimeScale rhs) const {
|
||||
[[nodiscard]] forceinline bool will_flush(LocalTimeScale rhs) const {
|
||||
if constexpr (!has_sequence_points<T>::value) {
|
||||
return false;
|
||||
}
|
||||
@ -141,60 +204,28 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
|
||||
}
|
||||
}
|
||||
|
||||
/// @returns A cached copy of the object's clocking preference.
|
||||
ClockingHint::Preference clocking_preference() const {
|
||||
return clocking_preference_;
|
||||
}
|
||||
|
||||
private:
|
||||
T object_;
|
||||
LocalTimeScale time_since_update_, time_until_event_;
|
||||
LocalTimeScale time_since_update_, time_until_event_, time_overrun_;
|
||||
bool is_flushed_ = true;
|
||||
bool did_flush_ = false;
|
||||
|
||||
template <typename S, typename = void> struct has_sequence_points : std::false_type {};
|
||||
template <typename S> struct has_sequence_points<S, decltype(void(std::declval<S &>().get_next_sequence_point()))> : std::true_type {};
|
||||
};
|
||||
|
||||
/*!
|
||||
A RealTimeActor presents the same interface as a JustInTimeActor but doesn't defer work.
|
||||
Time added will be performed immediately.
|
||||
|
||||
Its primary purpose is to allow consumers to remain flexible in their scheduling.
|
||||
*/
|
||||
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class RealTimeActor {
|
||||
public:
|
||||
template<typename... Args> RealTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
|
||||
|
||||
forceinline void operator += (const LocalTimeScale &rhs) {
|
||||
if constexpr (multiplier == 1 && divider == 1) {
|
||||
object_.run_for(TargetTimeScale(rhs));
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (multiplier == 1) {
|
||||
accumulated_time_ += rhs;
|
||||
} else {
|
||||
accumulated_time_ += rhs * multiplier;
|
||||
}
|
||||
|
||||
if constexpr (divider == 1) {
|
||||
const auto duration = accumulated_time_.template flush<TargetTimeScale>();
|
||||
object_.run_for(duration);
|
||||
} else {
|
||||
const auto duration = accumulated_time_.template divide<TargetTimeScale>(LocalTimeScale(divider));
|
||||
if(duration > TargetTimeScale(0))
|
||||
object_.run_for(duration);
|
||||
}
|
||||
ClockingHint::Preference clocking_preference_ = ClockingHint::Preference::JustInTime;
|
||||
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference clocking) {
|
||||
clocking_preference_ = clocking;
|
||||
}
|
||||
|
||||
forceinline T *operator->() { return &object_; }
|
||||
forceinline const T *operator->() const { return &object_; }
|
||||
forceinline T *last_valid() { return &object_; }
|
||||
forceinline void flush() {}
|
||||
|
||||
private:
|
||||
T object_;
|
||||
LocalTimeScale accumulated_time_;
|
||||
};
|
||||
|
||||
/*!
|
||||
A AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue.
|
||||
An AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue.
|
||||
Any time the amount of accumulated time crosses a threshold provided at construction time,
|
||||
the object will be updated on the AsyncTaskQueue.
|
||||
*/
|
||||
|
@ -708,9 +708,9 @@ HalfCycles Base::half_cycles_before_internal_cycles(int internal_cycles) {
|
||||
return HalfCycles(((internal_cycles << 2) + (2 - cycles_error_)) / 3);
|
||||
}
|
||||
|
||||
HalfCycles TMS9918::get_time_until_interrupt() {
|
||||
if(!generate_interrupts_ && !enable_line_interrupts_) return HalfCycles(-1);
|
||||
if(get_interrupt_line()) return HalfCycles(0);
|
||||
HalfCycles TMS9918::get_next_sequence_point() {
|
||||
if(!generate_interrupts_ && !enable_line_interrupts_) return HalfCycles::max();
|
||||
if(get_interrupt_line()) return HalfCycles::max();
|
||||
|
||||
// Calculate the amount of time until the next end-of-frame interrupt.
|
||||
const int frame_length = 342 * mode_timing_.total_lines;
|
||||
|
@ -75,13 +75,13 @@ class TMS9918: public Base {
|
||||
void latch_horizontal_counter();
|
||||
|
||||
/*!
|
||||
Returns the amount of time until @c get_interrupt_line would next return true if
|
||||
Returns the amount of time until @c get_interrupt_line would next change if
|
||||
there are no interceding calls to @c write or to @c read.
|
||||
|
||||
If get_interrupt_line is true now, returns zero. If get_interrupt_line would
|
||||
never return true, returns -1.
|
||||
If get_interrupt_line is true now of if get_interrupt_line would
|
||||
never return true, returns HalfCycles::max().
|
||||
*/
|
||||
HalfCycles get_time_until_interrupt();
|
||||
HalfCycles get_next_sequence_point();
|
||||
|
||||
/*!
|
||||
Returns the amount of time until the nominated line interrupt position is
|
||||
|
@ -1093,10 +1093,10 @@ class ConcreteMachine:
|
||||
// MARK: - Other components.
|
||||
|
||||
Apple::Clock::ParallelClock clock_;
|
||||
JustInTimeActor<Apple::IIgs::Video::Video, 1, 2, Cycles> video_; // i.e. run video at 7Mhz.
|
||||
JustInTimeActor<Apple::IIgs::ADB::GLU, 1, 4, Cycles> adb_glu_; // i.e. 3,579,545Mhz.
|
||||
JustInTimeActor<Apple::IIgs::Video::Video, Cycles, 1, 2> video_; // i.e. run video at 7Mhz.
|
||||
JustInTimeActor<Apple::IIgs::ADB::GLU, Cycles, 1, 4> adb_glu_; // i.e. 3,579,545Mhz.
|
||||
Zilog::SCC::z8530 scc_;
|
||||
JustInTimeActor<Apple::IWM, 1, 2, Cycles> iwm_;
|
||||
JustInTimeActor<Apple::IWM, Cycles, 1, 2> iwm_;
|
||||
Cycles cycles_since_clock_tick_;
|
||||
Apple::Macintosh::DoubleDensityDrive drives35_[2];
|
||||
Apple::Disk::DiskIIDrive drives525_[2];
|
||||
|
@ -576,7 +576,7 @@ class MemoryMap {
|
||||
if(region.write) { \
|
||||
region.write[address] = *value; \
|
||||
const bool _mm_is_shadowed = IsShadowed(map, region, address); \
|
||||
map.shadow_base[is_shadowed][(®ion.write[address] - map.ram_base) & map.shadow_mask[_mm_is_shadowed]] = *value; \
|
||||
map.shadow_base[_mm_is_shadowed][(®ion.write[address] - map.ram_base) & map.shadow_mask[_mm_is_shadowed]] = *value; \
|
||||
}
|
||||
|
||||
// Quick notes on ::IsShadowed contortions:
|
||||
|
@ -647,7 +647,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
return mouse_;
|
||||
}
|
||||
|
||||
using IWMActor = JustInTimeActor<IWM, 1, 1, HalfCycles, Cycles>;
|
||||
using IWMActor = JustInTimeActor<IWM>;
|
||||
|
||||
class VIAPortHandler: public MOS::MOS6522::PortHandler {
|
||||
public:
|
||||
|
@ -479,9 +479,9 @@ class ConcreteMachine:
|
||||
JustInTimeActor<Video> video_;
|
||||
|
||||
// The MFP runs at 819200/2673749ths of the CPU clock rate.
|
||||
JustInTimeActor<Motorola::MFP68901::MFP68901, 819200, 2673749> mfp_;
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, 16> keyboard_acia_;
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, 16> midi_acia_;
|
||||
JustInTimeActor<Motorola::MFP68901::MFP68901, HalfCycles, 819200, 2673749> mfp_;
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, HalfCycles, 16> keyboard_acia_;
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, HalfCycles, 16> midi_acia_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910<false> ay_;
|
||||
|
@ -215,7 +215,9 @@ class ConcreteMachine:
|
||||
}
|
||||
const HalfCycles length = cycle.length + penalty;
|
||||
|
||||
vdp_ += length;
|
||||
if(vdp_ += length) {
|
||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||
}
|
||||
time_since_sn76489_update_ += length;
|
||||
|
||||
// Act only if necessary.
|
||||
@ -263,7 +265,6 @@ class ConcreteMachine:
|
||||
case 5:
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 7: {
|
||||
@ -304,7 +305,6 @@ class ConcreteMachine:
|
||||
case 5:
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 7:
|
||||
@ -341,13 +341,6 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
if(time_until_interrupt_ > 0) {
|
||||
time_until_interrupt_ -= length;
|
||||
if(time_until_interrupt_ <= HalfCycles(0)) {
|
||||
z80_.set_non_maskable_interrupt_line(true, time_until_interrupt_);
|
||||
}
|
||||
}
|
||||
|
||||
return penalty;
|
||||
}
|
||||
|
||||
@ -384,7 +377,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
||||
JustInTimeActor<TI::TMS::TMS9918, 1, 1, HalfCycles> vdp_;
|
||||
JustInTimeActor<TI::TMS::TMS9918> vdp_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
@ -408,7 +401,6 @@ class ConcreteMachine:
|
||||
bool joysticks_in_keypad_mode_ = false;
|
||||
|
||||
HalfCycles time_since_sn76489_update_;
|
||||
HalfCycles time_until_interrupt_;
|
||||
|
||||
Analyser::Dynamic::ConfidenceCounter confidence_counter_;
|
||||
int pc_zero_accesses_ = 0;
|
||||
|
@ -25,6 +25,8 @@
|
||||
#include "../Utility/Typer.hpp"
|
||||
#include "../../Analyser/Static/Acorn/Target.hpp"
|
||||
|
||||
#include "../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include "Interrupts.hpp"
|
||||
#include "Keyboard.hpp"
|
||||
#include "Plus3.hpp"
|
||||
@ -54,7 +56,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
scsi_bus_(4'000'000),
|
||||
hard_drive_(scsi_bus_, 0),
|
||||
scsi_device_(scsi_bus_.add_device()),
|
||||
video_output_(ram_),
|
||||
video_(ram_),
|
||||
sound_generator_(audio_queue_),
|
||||
speaker_(sound_generator_) {
|
||||
memset(key_states_, 0, sizeof(key_states_));
|
||||
@ -230,13 +232,15 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
if(isReadOperation(operation)) {
|
||||
*value = ram_[address];
|
||||
} else {
|
||||
if(address >= video_access_range_.low_address && address <= video_access_range_.high_address) update_display();
|
||||
if(address >= video_access_range_.low_address && address <= video_access_range_.high_address) {
|
||||
video_.flush();
|
||||
}
|
||||
ram_[address] = *value;
|
||||
}
|
||||
|
||||
// for the entire frame, RAM is accessible only on odd cycles; in modes below 4
|
||||
// it's also accessible only outside of the pixel regions
|
||||
cycles += video_output_.get_cycles_until_next_ram_availability(int(cycles_since_display_update_.as_integral()) + 1);
|
||||
// For the entire frame, RAM is accessible only on odd cycles; in modes below 4
|
||||
// it's also accessible only outside of the pixel regions.
|
||||
cycles += video_.last_valid()->get_cycles_until_next_ram_availability(video_.time_since_flush().template as<int>() + 1);
|
||||
} else {
|
||||
switch(address & 0xff0f) {
|
||||
case 0xfe00:
|
||||
@ -274,10 +278,8 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
case 0xfe08: case 0xfe09: case 0xfe0a: case 0xfe0b:
|
||||
case 0xfe0c: case 0xfe0d: case 0xfe0e: case 0xfe0f:
|
||||
if(!isReadOperation(operation)) {
|
||||
update_display();
|
||||
video_output_.write(address, *value);
|
||||
video_access_range_ = video_output_.get_memory_access_range();
|
||||
queue_next_display_interrupt();
|
||||
video_->write(address, *value);
|
||||
video_access_range_ = video_.last_valid()->get_memory_access_range();
|
||||
}
|
||||
break;
|
||||
case 0xfe04:
|
||||
@ -425,14 +427,14 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
(address == 0xf0a8) // 0xf0a8 is from where a service call would normally be
|
||||
// dispatched; we can check whether it would be call 14
|
||||
// (i.e. read byte) and, if so, whether the OS was about to
|
||||
// issue a read byte call to a ROM despite being the tape
|
||||
// issue a read byte call to a ROM despite the tape
|
||||
// FS being selected. If so then this is a get byte that
|
||||
// we should service synthetically. Put the byte into Y
|
||||
// and set A to zero to report that action was taken, then
|
||||
// allow the PC read to return an RTS.
|
||||
)
|
||||
) {
|
||||
uint8_t service_call = uint8_t(m6502_.get_value_of_register(CPU::MOS6502::Register::X));
|
||||
const auto service_call = uint8_t(m6502_.get_value_of_register(CPU::MOS6502::Register::X));
|
||||
if(address == 0xf0a8) {
|
||||
if(!ram_[0x247] && service_call == 14) {
|
||||
tape_.set_delegate(nullptr);
|
||||
@ -441,7 +443,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
tape_.clear_interrupts(Interrupt::ReceiveDataFull);
|
||||
while(!tape_.get_tape()->is_at_end()) {
|
||||
tape_.run_for_input_pulse();
|
||||
cycles_left_while_plausibly_in_data--;
|
||||
--cycles_left_while_plausibly_in_data;
|
||||
if(!cycles_left_while_plausibly_in_data) fast_load_is_in_data_ = false;
|
||||
if( (tape_.get_interrupt_status() & Interrupt::ReceiveDataFull) &&
|
||||
(fast_load_is_in_data_ || tape_.get_data_register() == 0x2a)
|
||||
@ -483,18 +485,14 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
cycles_since_display_update_ += Cycles(int(cycles));
|
||||
if(video_ += Cycles(int(cycles))) {
|
||||
signal_interrupt(video_.last_valid()->get_interrupts());
|
||||
}
|
||||
|
||||
cycles_since_audio_update_ += Cycles(int(cycles));
|
||||
if(cycles_since_audio_update_ > Cycles(16384)) update_audio();
|
||||
tape_.run_for(Cycles(int(cycles)));
|
||||
|
||||
cycles_until_display_interrupt_ -= cycles;
|
||||
if(cycles_until_display_interrupt_ < 0) {
|
||||
signal_interrupt(next_display_interrupt_);
|
||||
update_display();
|
||||
queue_next_display_interrupt();
|
||||
}
|
||||
|
||||
if(typer_) typer_->run_for(Cycles(int(cycles)));
|
||||
if(plus3_) plus3_->run_for(Cycles(4*int(cycles)));
|
||||
if(shift_restart_counter_) {
|
||||
@ -517,25 +515,25 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
}
|
||||
|
||||
forceinline void flush() {
|
||||
update_display();
|
||||
video_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
video_output_.set_scan_target(scan_target);
|
||||
video_.last_valid()->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_output_.get_scaled_scan_status();
|
||||
return video_.last_valid()->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
video_output_.set_display_type(display_type);
|
||||
video_.last_valid()->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Display::DisplayType get_display_type() const final {
|
||||
return video_output_.get_display_type();
|
||||
return video_.last_valid()->get_display_type();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
@ -693,18 +691,6 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: - Work deferral updates.
|
||||
inline void update_display() {
|
||||
if(cycles_since_display_update_ > 0) {
|
||||
video_output_.run_for(cycles_since_display_update_.flush<Cycles>());
|
||||
}
|
||||
}
|
||||
|
||||
inline void queue_next_display_interrupt() {
|
||||
VideoOutput::Interrupt next_interrupt = video_output_.get_next_interrupt();
|
||||
cycles_until_display_interrupt_ = next_interrupt.cycles;
|
||||
next_display_interrupt_ = next_interrupt.interrupt;
|
||||
}
|
||||
|
||||
inline void update_audio() {
|
||||
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(SoundGenerator::clock_rate_divider)));
|
||||
}
|
||||
@ -754,10 +740,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
Electron::KeyboardMapper keyboard_mapper_;
|
||||
|
||||
// Counters related to simultaneous subsystems
|
||||
Cycles cycles_since_display_update_ = 0;
|
||||
Cycles cycles_since_audio_update_ = 0;
|
||||
int cycles_until_display_interrupt_ = 0;
|
||||
Interrupt next_display_interrupt_ = Interrupt::RealTimeClock;
|
||||
VideoOutput::Range video_access_range_ = {0, 0xffff};
|
||||
|
||||
// Tape
|
||||
@ -794,7 +777,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// Outputs
|
||||
VideoOutput video_output_;
|
||||
JustInTimeActor<VideoOutput, Cycles> video_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
SoundGenerator sound_generator_;
|
||||
|
@ -234,7 +234,23 @@ void VideoOutput::output_pixels(int number_of_cycles) {
|
||||
|
||||
void VideoOutput::run_for(const Cycles cycles) {
|
||||
int number_of_cycles = int(cycles.as_integral());
|
||||
const auto start_position = output_position_;
|
||||
output_position_ = (output_position_ + number_of_cycles) % cycles_per_frame;
|
||||
|
||||
if(
|
||||
(start_position < real_time_clock_interrupt_1 && output_position_ >= real_time_clock_interrupt_1) ||
|
||||
(start_position < real_time_clock_interrupt_2 && output_position_ >= real_time_clock_interrupt_2)
|
||||
) {
|
||||
interrupts_ = Electron::Interrupt(interrupts_ | Electron::Interrupt::RealTimeClock);
|
||||
}
|
||||
|
||||
if(
|
||||
(start_position < display_end_interrupt_1 && output_position_ >= display_end_interrupt_1) ||
|
||||
(start_position < display_end_interrupt_2 && output_position_ >= display_end_interrupt_2)
|
||||
) {
|
||||
interrupts_ = Electron::Interrupt(interrupts_ | Electron::Interrupt::DisplayEnd);
|
||||
}
|
||||
|
||||
while(number_of_cycles) {
|
||||
int draw_action_length = screen_map_[screen_map_pointer_].length;
|
||||
int time_left_in_action = std::min(number_of_cycles, draw_action_length - cycles_into_draw_action_);
|
||||
@ -354,36 +370,30 @@ void VideoOutput::setup_base_address() {
|
||||
|
||||
// MARK: - Interrupts
|
||||
|
||||
VideoOutput::Interrupt VideoOutput::get_next_interrupt() {
|
||||
VideoOutput::Interrupt interrupt;
|
||||
|
||||
Cycles VideoOutput::get_next_sequence_point() {
|
||||
if(output_position_ < real_time_clock_interrupt_1) {
|
||||
interrupt.cycles = real_time_clock_interrupt_1 - output_position_;
|
||||
interrupt.interrupt = RealTimeClock;
|
||||
return interrupt;
|
||||
return real_time_clock_interrupt_1 - output_position_;
|
||||
}
|
||||
|
||||
if(output_position_ < display_end_interrupt_1) {
|
||||
interrupt.cycles = display_end_interrupt_1 - output_position_;
|
||||
interrupt.interrupt = DisplayEnd;
|
||||
return interrupt;
|
||||
return display_end_interrupt_1 - output_position_;
|
||||
}
|
||||
|
||||
if(output_position_ < real_time_clock_interrupt_2) {
|
||||
interrupt.cycles = real_time_clock_interrupt_2 - output_position_;
|
||||
interrupt.interrupt = RealTimeClock;
|
||||
return interrupt;
|
||||
return real_time_clock_interrupt_2 - output_position_;
|
||||
}
|
||||
|
||||
if(output_position_ < display_end_interrupt_2) {
|
||||
interrupt.cycles = display_end_interrupt_2 - output_position_;
|
||||
interrupt.interrupt = DisplayEnd;
|
||||
return interrupt;
|
||||
return display_end_interrupt_2 - output_position_;
|
||||
}
|
||||
|
||||
interrupt.cycles = real_time_clock_interrupt_1 + cycles_per_frame - output_position_;
|
||||
interrupt.interrupt = RealTimeClock;
|
||||
return interrupt;
|
||||
return real_time_clock_interrupt_1 + cycles_per_frame - output_position_;
|
||||
}
|
||||
|
||||
Electron::Interrupt VideoOutput::get_interrupts() {
|
||||
const auto interrupts = interrupts_;
|
||||
interrupts_ = Electron::Interrupt(0);
|
||||
return interrupts;
|
||||
}
|
||||
|
||||
// MARK: - RAM timing and access information
|
||||
|
@ -54,15 +54,6 @@ class VideoOutput {
|
||||
*/
|
||||
void write(int address, uint8_t value);
|
||||
|
||||
/*!
|
||||
Describes an interrupt the video hardware will generate by its identity and scheduling time.
|
||||
*/
|
||||
struct Interrupt {
|
||||
/// The interrupt that will be signalled.
|
||||
Electron::Interrupt interrupt;
|
||||
/// The number of cycles until it is signalled.
|
||||
int cycles;
|
||||
};
|
||||
/*!
|
||||
@returns the next interrupt that should be generated as a result of the video hardware.
|
||||
The time until signalling returned is the number of cycles after the final one triggered
|
||||
@ -70,7 +61,12 @@ class VideoOutput {
|
||||
|
||||
This result may be mutated by calls to @c write.
|
||||
*/
|
||||
Interrupt get_next_interrupt();
|
||||
Cycles get_next_sequence_point();
|
||||
|
||||
/*!
|
||||
@returns a bit mask of all interrupts that have been triggered since the last call to get_interrupt().
|
||||
*/
|
||||
Electron::Interrupt get_interrupts();
|
||||
|
||||
/*!
|
||||
@returns the number of cycles after (final cycle of last run_for batch + @c from_time)
|
||||
@ -136,6 +132,8 @@ class VideoOutput {
|
||||
void emplace_pixel_line();
|
||||
std::size_t screen_map_pointer_ = 0;
|
||||
int cycles_into_draw_action_ = 0;
|
||||
|
||||
Electron::Interrupt interrupts_ = Electron::Interrupt(0);
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -419,7 +419,9 @@ class ConcreteMachine:
|
||||
// but otherwise runs without pause.
|
||||
const HalfCycles addition((cycle.operation == CPU::Z80::PartialMachineCycle::ReadOpcode) ? 2 : 0);
|
||||
const HalfCycles total_length = addition + cycle.length;
|
||||
vdp_ += total_length;
|
||||
if(vdp_ += total_length) {
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line(), vdp_.last_sequence_point_overrun());
|
||||
}
|
||||
time_since_ay_update_ += total_length;
|
||||
memory_slots_[0].cycles_since_update += total_length;
|
||||
memory_slots_[1].cycles_since_update += total_length;
|
||||
@ -520,7 +522,6 @@ class ConcreteMachine:
|
||||
case 0x98: case 0x99:
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 0xa2:
|
||||
@ -545,7 +546,6 @@ class ConcreteMachine:
|
||||
case 0x98: case 0x99:
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
|
||||
case 0xa0: case 0xa1:
|
||||
@ -606,12 +606,6 @@ class ConcreteMachine:
|
||||
if(!tape_player_is_sleeping_)
|
||||
tape_player_.run_for(int(cycle.length.as_integral()));
|
||||
|
||||
if(time_until_interrupt_ > 0) {
|
||||
time_until_interrupt_ -= total_length;
|
||||
if(time_until_interrupt_ <= HalfCycles(0)) {
|
||||
z80_.set_interrupt_line(true, time_until_interrupt_);
|
||||
}
|
||||
}
|
||||
return addition;
|
||||
}
|
||||
|
||||
@ -785,7 +779,6 @@ class ConcreteMachine:
|
||||
uint8_t unpopulated_[8192];
|
||||
|
||||
HalfCycles time_since_ay_update_;
|
||||
HalfCycles time_until_interrupt_;
|
||||
|
||||
uint8_t key_states_[16];
|
||||
int selected_key_line_ = 0;
|
||||
|
@ -214,7 +214,9 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
vdp_ += cycle.length;
|
||||
if(vdp_ += cycle.length) {
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line(), vdp_.last_sequence_point_overrun());
|
||||
}
|
||||
time_since_sn76489_update_ += cycle.length;
|
||||
|
||||
if(cycle.is_terminal()) {
|
||||
@ -266,7 +268,6 @@ class ConcreteMachine:
|
||||
case 0x80: case 0x81:
|
||||
*cycle.value = vdp_->read(address);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
case 0xc0: {
|
||||
if(memory_control_ & 0x4) {
|
||||
@ -330,7 +331,6 @@ class ConcreteMachine:
|
||||
case 0x80: case 0x81: // i.e. ports 0x80–0xbf.
|
||||
vdp_->write(address, *cycle.value);
|
||||
z80_.set_interrupt_line(vdp_->get_interrupt_line());
|
||||
time_until_interrupt_ = vdp_->get_time_until_interrupt();
|
||||
break;
|
||||
case 0xc1: case 0xc0: // i.e. ports 0xc0–0xff.
|
||||
if(has_fm_audio_) {
|
||||
@ -374,13 +374,6 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
if(time_until_interrupt_ > 0) {
|
||||
time_until_interrupt_ -= cycle.length;
|
||||
if(time_until_interrupt_ <= HalfCycles(0)) {
|
||||
z80_.set_interrupt_line(true, time_until_interrupt_);
|
||||
}
|
||||
}
|
||||
|
||||
// The pause button is debounced and takes effect only one line before pixels
|
||||
// begin; time_until_debounce_ keeps track of the time until then.
|
||||
time_until_debounce_ -= cycle.length;
|
||||
@ -505,7 +498,6 @@ class ConcreteMachine:
|
||||
bool reset_is_pressed_ = false, pause_is_pressed_ = false;
|
||||
|
||||
HalfCycles time_since_sn76489_update_;
|
||||
HalfCycles time_until_interrupt_;
|
||||
HalfCycles time_until_debounce_;
|
||||
|
||||
uint8_t ram_[8*1024];
|
||||
|
@ -34,6 +34,8 @@
|
||||
|
||||
#include "../../Analyser/Static/Oric/Target.hpp"
|
||||
|
||||
#include "../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
@ -217,7 +219,6 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
|
||||
public Storage::Tape::BinaryTapePlayer::Delegate,
|
||||
public DiskController::Delegate,
|
||||
public ClockingHint::Observer,
|
||||
public Activity::Source,
|
||||
public Machine,
|
||||
public Keyboard::SpecialKeyHandler {
|
||||
@ -225,7 +226,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::Oric::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||||
m6502_(*this),
|
||||
video_output_(ram_),
|
||||
video_(ram_),
|
||||
ay8910_(GI::AY38910::Personality::AY38910, audio_queue_),
|
||||
speaker_(ay8910_),
|
||||
via_port_handler_(audio_queue_, ay8910_, speaker_, tape_player_, keyboard_),
|
||||
@ -247,10 +248,6 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
ram_[c] |= 0x40;
|
||||
}
|
||||
|
||||
if constexpr (disk_interface == DiskInterface::Pravetz) {
|
||||
diskii_.set_clocking_hint_observer(this);
|
||||
}
|
||||
|
||||
const std::string machine_name = "Oric";
|
||||
std::vector<ROMMachine::ROM> rom_names = { {machine_name, "the Oric colour ROM", "colour.rom", 128, 0xd50fca65} };
|
||||
switch(target.rom) {
|
||||
@ -292,7 +289,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
}
|
||||
}
|
||||
|
||||
video_output_.set_colour_rom(*roms[0]);
|
||||
video_->set_colour_rom(*roms[0]);
|
||||
rom_ = std::move(*roms[1]);
|
||||
|
||||
switch(disk_interface) {
|
||||
@ -313,7 +310,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
pravetz_rom_ = std::move(*roms[2]);
|
||||
pravetz_rom_.resize(512);
|
||||
|
||||
diskii_.set_state_machine(*roms[diskii_state_machine_index]);
|
||||
diskii_->set_state_machine(*roms[diskii_state_machine_index]);
|
||||
} break;
|
||||
}
|
||||
|
||||
@ -402,7 +399,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
case DiskInterface::BD500: inserted |= insert_disks(media, bd500_, 4); break;
|
||||
case DiskInterface::Jasmin: inserted |= insert_disks(media, jasmin_, 4); break;
|
||||
case DiskInterface::Microdisc: inserted |= insert_disks(media, microdisc_, 4); break;
|
||||
case DiskInterface::Pravetz: inserted |= insert_disks(media, diskii_, 2); break;
|
||||
case DiskInterface::Pravetz: inserted |= insert_disks(media, *diskii_.last_valid(), 2); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@ -474,9 +471,8 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
}
|
||||
}
|
||||
} else {
|
||||
flush_diskii();
|
||||
const int disk_value = diskii_.read_address(address);
|
||||
if(!isWriteOperation(operation) && disk_value != diskii_.DidNotLoad) *value = uint8_t(disk_value);
|
||||
const int disk_value = diskii_->read_address(address);
|
||||
if(!isWriteOperation(operation) && disk_value != Apple::DiskII::DidNotLoad) *value = uint8_t(disk_value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -485,7 +481,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
if(!isWriteOperation(operation))
|
||||
*value = ram_[address];
|
||||
else {
|
||||
if(address >= 0x9800 && address <= 0xc000) update_video();
|
||||
if(address >= 0x9800 && address <= 0xc000) video_.flush();
|
||||
ram_[address] = *value;
|
||||
}
|
||||
}
|
||||
@ -508,7 +504,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
bd500_.run_for(Cycles(9)); // i.e. effective clock rate of 9Mhz.
|
||||
break;
|
||||
case DiskInterface::Jasmin:
|
||||
jasmin_.run_for(Cycles(8));; // i.e. effective clock rate of 8Mhz.
|
||||
jasmin_.run_for(Cycles(8)); // i.e. effective clock rate of 8Mhz.
|
||||
|
||||
// Jasmin autostart hack: wait for a period, then trigger a reset, having forced
|
||||
// the Jasmin to page its ROM in first. I assume the latter being what the Jasmin's
|
||||
@ -521,43 +517,41 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
}
|
||||
break;
|
||||
case DiskInterface::Microdisc:
|
||||
microdisc_.run_for(Cycles(8));; // i.e. effective clock rate of 8Mhz.
|
||||
microdisc_.run_for(Cycles(8)); // i.e. effective clock rate of 8Mhz.
|
||||
break;
|
||||
case DiskInterface::Pravetz:
|
||||
if(diskii_clocking_preference_ == ClockingHint::Preference::RealTime) {
|
||||
diskii_.set_data_input(*value);
|
||||
diskii_.run_for(Cycles(2));; // i.e. effective clock rate of 2Mhz.
|
||||
} else {
|
||||
cycles_since_diskii_update_ += Cycles(2);
|
||||
if(diskii_.clocking_preference() == ClockingHint::Preference::RealTime) {
|
||||
diskii_->set_data_input(*value);
|
||||
}
|
||||
diskii_ += Cycles(2); // i.e. effective clock rate of 2Mhz.
|
||||
break;
|
||||
}
|
||||
cycles_since_video_update_++;
|
||||
|
||||
video_ += Cycles(1);
|
||||
return Cycles(1);
|
||||
}
|
||||
|
||||
forceinline void flush() {
|
||||
update_video();
|
||||
video_.flush();
|
||||
via_.flush();
|
||||
flush_diskii();
|
||||
diskii_.flush();
|
||||
}
|
||||
|
||||
// to satisfy CRTMachine::Machine
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
video_output_.set_scan_target(scan_target);
|
||||
video_.last_valid()->set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
return video_output_.get_scaled_scan_status();
|
||||
return video_.last_valid()->get_scaled_scan_status();
|
||||
}
|
||||
|
||||
void set_display_type(Outputs::Display::DisplayType display_type) final {
|
||||
video_output_.set_display_type(display_type);
|
||||
video_.last_valid()->set_display_type(display_type);
|
||||
}
|
||||
|
||||
Outputs::Display::DisplayType get_display_type() const final {
|
||||
return video_output_.get_display_type();
|
||||
return video_.last_valid()->get_display_type();
|
||||
}
|
||||
|
||||
Outputs::Speaker::Speaker *get_speaker() final {
|
||||
@ -644,15 +638,11 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
microdisc_.set_activity_observer(observer);
|
||||
break;
|
||||
case DiskInterface::Pravetz:
|
||||
diskii_.set_activity_observer(observer);
|
||||
diskii_->set_activity_observer(observer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) final {
|
||||
diskii_clocking_preference_ = diskii_.preferred_clocking();
|
||||
}
|
||||
|
||||
private:
|
||||
const uint16_t basic_invisible_ram_top_ = 0xffff;
|
||||
const uint16_t basic_visible_ram_top_ = 0xbfff;
|
||||
@ -662,17 +652,13 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
// RAM and ROM
|
||||
std::vector<uint8_t> rom_, disk_rom_;
|
||||
uint8_t ram_[65536];
|
||||
Cycles cycles_since_video_update_;
|
||||
inline void update_video() {
|
||||
video_output_.run_for(cycles_since_video_update_.flush<Cycles>());
|
||||
}
|
||||
|
||||
// ROM bookkeeping
|
||||
uint16_t tape_get_byte_address_ = 0, tape_speed_address_ = 0;
|
||||
int keyboard_read_count_ = 0;
|
||||
|
||||
// Outputs
|
||||
VideoOutput video_output_;
|
||||
JustInTimeActor<VideoOutput, Cycles> video_;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910<false> ay8910_;
|
||||
@ -700,14 +686,9 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
BD500 bd500_;
|
||||
|
||||
// the Pravetz/Disk II, if in use.
|
||||
Apple::DiskII diskii_;
|
||||
Cycles cycles_since_diskii_update_;
|
||||
void flush_diskii() {
|
||||
diskii_.run_for(cycles_since_diskii_update_.flush<Cycles>());
|
||||
}
|
||||
JustInTimeActor<Apple::DiskII, Cycles> diskii_;
|
||||
std::vector<uint8_t> pravetz_rom_;
|
||||
std::size_t pravetz_rom_base_pointer_ = 0;
|
||||
ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime;
|
||||
|
||||
// Overlay RAM
|
||||
uint16_t ram_top_ = basic_visible_ram_top_;
|
||||
|
@ -72,7 +72,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
#define SHIFT(...) {KeyShift, __VA_ARGS__, MachineTypes::MappedKeyboardMachine::KeyEndSequence}
|
||||
#define SYMSHIFT(...) {KeySymbolShift, __VA_ARGS__, MachineTypes::MappedKeyboardMachine::KeyEndSequence}
|
||||
#define X {MachineTypes::MappedKeyboardMachine::KeyNotMapped}
|
||||
constexpr KeySequence spectrum_key_sequences[] = {
|
||||
static constexpr KeySequence spectrum_key_sequences[] = {
|
||||
/* NUL */ X, /* SOH */ X,
|
||||
/* STX */ X, /* ETX */ X,
|
||||
/* EOT */ X, /* ENQ */ X,
|
||||
@ -137,7 +137,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
/* z */ KEYS(KeyZ),
|
||||
};
|
||||
|
||||
constexpr KeySequence zx81_key_sequences[] = {
|
||||
static constexpr KeySequence zx81_key_sequences[] = {
|
||||
/* NUL */ X, /* SOH */ X,
|
||||
/* STX */ X, /* ETX */ X,
|
||||
/* EOT */ X, /* ENQ */ X,
|
||||
@ -203,7 +203,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
|
||||
/* | */ X, /* } */ X,
|
||||
};
|
||||
|
||||
static KeySequence zx80_key_sequences[] = {
|
||||
static constexpr KeySequence zx80_key_sequences[] = {
|
||||
/* NUL */ X, /* SOH */ X,
|
||||
/* STX */ X, /* ETX */ X,
|
||||
/* EOT */ X, /* ENQ */ X,
|
||||
|
@ -53,14 +53,14 @@ template <VideoTiming timing> class Video {
|
||||
// Number of lines comprising a whole frame. Will be 311 or 312.
|
||||
int lines_per_frame;
|
||||
|
||||
// Number of cycles after first pixel fetch at which interrupt is first signalled.
|
||||
int interrupt_time;
|
||||
|
||||
// Number of cycles before first pixel fetch that contention starts to be applied.
|
||||
int contention_leadin;
|
||||
// Period in a line for which contention is applied.
|
||||
int contention_duration;
|
||||
|
||||
// Number of cycles after first pixel fetch at which interrupt is first signalled.
|
||||
int interrupt_time;
|
||||
|
||||
// Contention to apply, in half-cycles, as a function of number of half cycles since
|
||||
// contention began.
|
||||
int delays[16];
|
||||
@ -88,11 +88,15 @@ template <VideoTiming timing> class Video {
|
||||
.cycles_per_line = 228 * 2,
|
||||
.lines_per_frame = 311,
|
||||
|
||||
.interrupt_time = 56545 * 2,
|
||||
|
||||
.contention_leadin = 2 * 2, // TODO: is this 2? Or 4? Or... ?
|
||||
// i.e. video fetching begins five cycles after the start of the
|
||||
// contended memory pattern below; that should put a clear two
|
||||
// cycles between a Z80 access and the first video fetch.
|
||||
.contention_leadin = 5 * 2,
|
||||
.contention_duration = 129 * 2,
|
||||
|
||||
// i.e. interrupt is first signalled 14368 cycles before the first video fetch.
|
||||
.interrupt_time = (228*311 - 14368) * 2,
|
||||
|
||||
.delays = {
|
||||
2, 1,
|
||||
0, 0,
|
||||
@ -311,7 +315,9 @@ template <VideoTiming timing> class Video {
|
||||
}
|
||||
|
||||
const int time_into_line = delay_time % timings.cycles_per_line;
|
||||
if(time_into_line >= timings.contention_duration) return 0;
|
||||
if(time_into_line >= timings.contention_duration) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return timings.delays[time_into_line & 15];
|
||||
}
|
||||
|
@ -39,6 +39,8 @@
|
||||
|
||||
#include "../../../ClockReceiver/JustInTime.hpp"
|
||||
|
||||
#include "../../../Processors/Z80/State/State.hpp"
|
||||
|
||||
#include "../Keyboard/Keyboard.hpp"
|
||||
|
||||
#include <array>
|
||||
@ -85,7 +87,6 @@ template<Model model> class ConcreteMachine:
|
||||
memcpy(rom_.data(), roms[0]->data(), std::min(rom_.size(), roms[0]->size()));
|
||||
|
||||
// Register for sleeping notifications.
|
||||
fdc_->set_clocking_hint_observer(this);
|
||||
tape_player_.set_clocking_hint_observer(this);
|
||||
|
||||
// Set up initial memory map.
|
||||
@ -185,37 +186,42 @@ template<Model model> class ConcreteMachine:
|
||||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||||
using PartialMachineCycle = CPU::Z80::PartialMachineCycle;
|
||||
|
||||
HalfCycles delay(0);
|
||||
const uint16_t address = cycle.address ? *cycle.address : 0x0000;
|
||||
|
||||
// Apply contention if necessary.
|
||||
if(
|
||||
is_contended_[address >> 14] &&
|
||||
cycle.operation >= PartialMachineCycle::ReadOpcodeStart &&
|
||||
cycle.operation <= PartialMachineCycle::WriteStart) {
|
||||
// Assumption here: the trigger for the ULA inserting a delay is the falling edge
|
||||
// of MREQ, which is always half a cycle into a read or write.
|
||||
//
|
||||
// TODO: somehow provide that information in the PartialMachineCycle?
|
||||
|
||||
const HalfCycles delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1));
|
||||
advance(cycle.length + delay);
|
||||
return delay;
|
||||
}
|
||||
|
||||
// For all other machine cycles, model the action as happening at the end of the machine cycle;
|
||||
// that means advancing time now.
|
||||
advance(cycle.length);
|
||||
|
||||
switch(cycle.operation) {
|
||||
default: break;
|
||||
|
||||
case PartialMachineCycle::ReadOpcodeStart:
|
||||
case PartialMachineCycle::ReadStart:
|
||||
case PartialMachineCycle::WriteStart:
|
||||
// Apply contention if necessary.
|
||||
//
|
||||
// Assumption here: the trigger for the ULA inserting a delay is the falling edge
|
||||
// of MREQ, which is always half a cycle into a read or write.
|
||||
//
|
||||
// TODO: somehow provide that information in the PartialMachineCycle?
|
||||
if(is_contended_[address >> 14]) {
|
||||
delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1));
|
||||
}
|
||||
break;
|
||||
|
||||
case PartialMachineCycle::ReadOpcode:
|
||||
// Fast loading: ROM version.
|
||||
//
|
||||
// The below patches over the 'LD-BYTES' routine from the 48kb ROM.
|
||||
if(use_fast_tape_hack_ && address == 0x0556 && read_pointers_[0] == &rom_[0xc000]) {
|
||||
if(perform_rom_ld_bytes()) {
|
||||
// Stop pressing enter, if neccessry.
|
||||
if(duration_to_press_enter_ > Cycles(0)) {
|
||||
duration_to_press_enter_ = Cycles(0);
|
||||
keyboard_.set_key_state(ZX::Keyboard::KeyEnter, false);
|
||||
}
|
||||
// The below patches over part of the 'LD-BYTES' routine from the 48kb ROM.
|
||||
if(use_fast_tape_hack_ && address == 0x056b && read_pointers_[0] == &rom_[0xc000]) {
|
||||
// Stop pressing enter, if neccessry.
|
||||
if(duration_to_press_enter_ > Cycles(0)) {
|
||||
duration_to_press_enter_ = Cycles(0);
|
||||
keyboard_.set_key_state(ZX::Keyboard::KeyEnter, false);
|
||||
}
|
||||
|
||||
if(perform_rom_ld_bytes_56b()) {
|
||||
*cycle.value = 0xc9; // i.e. RET.
|
||||
break;
|
||||
}
|
||||
@ -225,7 +231,7 @@ template<Model model> class ConcreteMachine:
|
||||
*cycle.value = read_pointers_[address >> 14][address];
|
||||
|
||||
if(is_contended_[address >> 14]) {
|
||||
video_.last_valid()->set_last_contended_area_access(*cycle.value);
|
||||
video_->set_last_contended_area_access(*cycle.value);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -239,7 +245,7 @@ template<Model model> class ConcreteMachine:
|
||||
|
||||
// Fill the floating bus buffer if this write is within the contended area.
|
||||
if(is_contended_[address >> 14]) {
|
||||
video_.last_valid()->set_last_contended_area_access(*cycle.value);
|
||||
video_->set_last_contended_area_access(*cycle.value);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -357,8 +363,7 @@ template<Model model> class ConcreteMachine:
|
||||
break;
|
||||
}
|
||||
|
||||
advance(cycle.length + delay);
|
||||
return delay;
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
private:
|
||||
@ -367,7 +372,7 @@ template<Model model> class ConcreteMachine:
|
||||
|
||||
video_ += duration;
|
||||
if(video_.did_flush()) {
|
||||
z80_.set_interrupt_line(video_.last_valid()->get_interrupt_line());
|
||||
z80_.set_interrupt_line(video_.last_valid()->get_interrupt_line(), video_.last_sequence_point_overrun());
|
||||
}
|
||||
|
||||
if(!tape_player_is_sleeping_) tape_player_.run_for(duration.as_integral());
|
||||
@ -384,7 +389,7 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
if constexpr (model == Model::Plus3) {
|
||||
if(!fdc_is_sleeping_) fdc_ += Cycles(duration.as_integral());
|
||||
fdc_ += Cycles(duration.as_integral());
|
||||
}
|
||||
|
||||
if(typer_) typer_->run_for(duration);
|
||||
@ -449,7 +454,6 @@ template<Model model> class ConcreteMachine:
|
||||
// MARK: - ClockingHint::Observer.
|
||||
|
||||
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) override {
|
||||
fdc_is_sleeping_ = fdc_.last_valid()->preferred_clocking() == ClockingHint::Preference::None;
|
||||
tape_player_is_sleeping_ = tape_player_.preferred_clocking() == ClockingHint::Preference::None;
|
||||
}
|
||||
|
||||
@ -625,31 +629,42 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// Reimplements the 'LD-BYTES' routine, as documented at
|
||||
// https://skoolkid.github.io/rom/asm/0556.html i.e.
|
||||
// https://skoolkid.github.io/rom/asm/0556.html but picking
|
||||
// up from address 56b i.e.
|
||||
//
|
||||
// In:
|
||||
// A: 0x00 or 0xff for block type;
|
||||
// F: carry set if loading, clear if verifying;
|
||||
// A': 0x00 or 0xff for block type;
|
||||
// F': carry set if loading, clear if verifying;
|
||||
// DE: block length;
|
||||
// IX: start address.
|
||||
//
|
||||
// Out:
|
||||
// F: carry set for success, clear for error.
|
||||
bool perform_rom_ld_bytes() {
|
||||
//
|
||||
// And, empirically:
|
||||
// IX: one beyond final address written;
|
||||
// DE: 0;
|
||||
// L: parity byte;
|
||||
// H: 0 for no error, 0xff for error;
|
||||
// A: same as H.
|
||||
// BC: ???
|
||||
bool perform_rom_ld_bytes_56b() {
|
||||
using Parser = Storage::Tape::ZXSpectrum::Parser;
|
||||
Parser parser(Parser::MachineType::ZXSpectrum);
|
||||
|
||||
using Register = CPU::Z80::Register;
|
||||
uint8_t flags = uint8_t(z80_.get_value_of_register(Register::Flags));
|
||||
uint8_t flags = uint8_t(z80_.get_value_of_register(Register::FlagsDash));
|
||||
if(!(flags & 1)) return false;
|
||||
|
||||
const uint8_t block_type = uint8_t(z80_.get_value_of_register(Register::A));
|
||||
const uint8_t block_type = uint8_t(z80_.get_value_of_register(Register::ADash));
|
||||
const auto block = parser.find_block(tape_player_.get_tape());
|
||||
if(!block || block_type != (*block).type) return false;
|
||||
|
||||
uint16_t length = z80_.get_value_of_register(Register::DE);
|
||||
uint16_t target = z80_.get_value_of_register(Register::IX);
|
||||
|
||||
flags = 0x93;
|
||||
uint8_t parity = 0x00;
|
||||
while(length--) {
|
||||
auto next = parser.get_byte(tape_player_.get_tape());
|
||||
if(!next) {
|
||||
@ -658,16 +673,30 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
write_pointers_[target >> 14][target] = *next;
|
||||
parity ^= *next;
|
||||
++target;
|
||||
}
|
||||
|
||||
auto stored_parity = parser.get_byte(tape_player_.get_tape());
|
||||
if(!stored_parity) {
|
||||
flags &= ~1;
|
||||
} else {
|
||||
z80_.set_value_of_register(Register::L, *stored_parity);
|
||||
}
|
||||
|
||||
z80_.set_value_of_register(Register::Flags, flags);
|
||||
z80_.set_value_of_register(Register::DE, length);
|
||||
z80_.set_value_of_register(Register::IX, target);
|
||||
|
||||
const uint8_t h = (flags & 1) ? 0x00 : 0xff;
|
||||
z80_.set_value_of_register(Register::H, h);
|
||||
z80_.set_value_of_register(Register::A, h);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// MARK: - Disc.
|
||||
JustInTimeActor<Amstrad::FDC, 1, 1, Cycles> fdc_;
|
||||
bool fdc_is_sleeping_ = false;
|
||||
JustInTimeActor<Amstrad::FDC, Cycles> fdc_;
|
||||
|
||||
// MARK: - Automatic startup.
|
||||
Cycles duration_to_press_enter_;
|
||||
|
@ -34,10 +34,10 @@
|
||||
vdp.write(1, 0x8a);
|
||||
|
||||
// Get time until interrupt.
|
||||
auto time_until_interrupt = vdp.get_time_until_interrupt().as_integral() - 1;
|
||||
auto time_until_interrupt = vdp.get_next_sequence_point().as_integral() - 1;
|
||||
|
||||
// Check that an interrupt is now scheduled.
|
||||
NSAssert(time_until_interrupt != -2, @"No interrupt scheduled");
|
||||
NSAssert(time_until_interrupt != HalfCycles::max().as_integral() - 1, @"No interrupt scheduled");
|
||||
NSAssert(time_until_interrupt > 0, @"Interrupt is scheduled in the past");
|
||||
|
||||
// Check interrupt flag isn't set prior to the reported time.
|
||||
@ -53,7 +53,7 @@
|
||||
NSAssert(!vdp.get_interrupt_line(), @"Interrupt wasn't reset by status read");
|
||||
|
||||
// Check interrupt flag isn't set prior to the reported time.
|
||||
time_until_interrupt = vdp.get_time_until_interrupt().as_integral() - 1;
|
||||
time_until_interrupt = vdp.get_next_sequence_point().as_integral() - 1;
|
||||
vdp.run_for(HalfCycles(time_until_interrupt));
|
||||
NSAssert(!vdp.get_interrupt_line(), @"Interrupt line went active early [2]");
|
||||
|
||||
@ -80,10 +80,10 @@
|
||||
|
||||
// Clear the pending interrupt and ask about the next one (i.e. the first one).
|
||||
vdp.read(1);
|
||||
auto time_until_interrupt = vdp.get_time_until_interrupt().as_integral() - 1;
|
||||
auto time_until_interrupt = vdp.get_next_sequence_point().as_integral() - 1;
|
||||
|
||||
// Check that an interrupt is now scheduled.
|
||||
NSAssert(time_until_interrupt != -2, @"No interrupt scheduled");
|
||||
NSAssert(time_until_interrupt != HalfCycles::max().as_integral() - 1, @"No interrupt scheduled");
|
||||
NSAssert(time_until_interrupt > 0, @"Interrupt is scheduled in the past");
|
||||
|
||||
// Check interrupt flag isn't set prior to the reported time.
|
||||
@ -114,20 +114,24 @@
|
||||
|
||||
// Now run through an entire frame...
|
||||
int half_cycles = 262*228*2;
|
||||
auto last_time_until_interrupt = vdp.get_time_until_interrupt().as_integral();
|
||||
auto last_time_until_interrupt = vdp.get_next_sequence_point().as_integral();
|
||||
while(half_cycles--) {
|
||||
// Validate that an interrupt happened if one was expected, and clear anything that's present.
|
||||
NSAssert(vdp.get_interrupt_line() == (last_time_until_interrupt == 0), @"Unexpected interrupt state change; expected %d but got %d; position %d %d @ %d", (last_time_until_interrupt == 0), vdp.get_interrupt_line(), c, with_eof, half_cycles);
|
||||
vdp.read(1);
|
||||
NSAssert(vdp.get_interrupt_line() == (last_time_until_interrupt == HalfCycles::max().as_integral()), @"Unexpected interrupt state change; expected %d but got %d; position %d %d @ %d", (last_time_until_interrupt == 0), vdp.get_interrupt_line(), c, with_eof, half_cycles);
|
||||
|
||||
if(vdp.get_interrupt_line()) {
|
||||
vdp.read(1);
|
||||
last_time_until_interrupt = 0;
|
||||
}
|
||||
|
||||
vdp.run_for(HalfCycles(1));
|
||||
|
||||
// Get the time until interrupt.
|
||||
auto time_until_interrupt = vdp.get_time_until_interrupt().as_integral();
|
||||
NSAssert(time_until_interrupt != -1, @"No interrupt scheduled; position %d %d @ %d", c, with_eof, half_cycles);
|
||||
auto time_until_interrupt = vdp.get_next_sequence_point().as_integral();
|
||||
NSAssert(time_until_interrupt != HalfCycles::max().as_integral() || vdp.get_interrupt_line(), @"No interrupt scheduled; position %d %d @ %d", c, with_eof, half_cycles);
|
||||
NSAssert(time_until_interrupt >= 0, @"Interrupt is scheduled in the past; position %d %d @ %d", c, with_eof, half_cycles);
|
||||
|
||||
if(last_time_until_interrupt) {
|
||||
if(last_time_until_interrupt > 1) {
|
||||
NSAssert(
|
||||
time_until_interrupt == (last_time_until_interrupt - 1),
|
||||
@"Discontinuity found in interrupt prediction; from %@ to %@; position %d %d @ %d",
|
||||
|
@ -212,6 +212,7 @@ std::string Reflection::Struct::description() const {
|
||||
for(const auto &key: all_keys()) {
|
||||
if(!is_first) stream << ", ";
|
||||
is_first = false;
|
||||
|
||||
stream << key << ": ";
|
||||
|
||||
const auto count = count_of(key);
|
||||
|
@ -12,6 +12,7 @@
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
|
||||
#define LOG_PREFIX "[UEF] "
|
||||
#include "../../../Outputs/Log.hpp"
|
||||
|
||||
// MARK: - ZLib extensions
|
||||
@ -155,7 +156,7 @@ void UEF::get_next_pulses() {
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG("!!! Skipping " << std::hex << next_chunk.id << std::endl);
|
||||
LOG("Skipping chunk of type " << PADHEX(4) << next_chunk.id);
|
||||
break;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user