1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-11-23 03:32:32 +00:00

Merge pull request #1141 from TomHarte/ConvertFromGrauw

Clean up further internal magic constants.
This commit is contained in:
Thomas Harte 2023-05-19 19:52:40 -04:00 committed by GitHub
commit dd3fc43bd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 165 additions and 119 deletions

View File

@ -35,12 +35,16 @@ Base<personality>::Base() :
// "For a line interrupt, /INT is pulled low 608 mclks into the appropriate scanline relative to pixel 0.
// This is 3 mclks before the rising edge of /HSYNC which starts the next scanline."
//
// i.e. it's 304 internal clocks after the end of the left border.
mode_timing_.line_interrupt_position = (LineLayout<personality>::EndOfLeftBorder + 304) % LineLayout<personality>::CyclesPerLine;
// For a frame interrupt, /INT is pulled low 607 mclks into scanline 192 (of scanlines 0 through 261) relative to pixel 0.
// This is 4 mclks before the rising edge of /HSYNC which starts the next scanline.
//
// i.e. it's 1/2 cycle before the line interrupt position, which I have rounded. Ugh.
mode_timing_.end_of_frame_interrupt_position.column = mode_timing_.line_interrupt_position - 1;
mode_timing_.end_of_frame_interrupt_position.row = 193;
mode_timing_.end_of_frame_interrupt_position.row = 192 + (LineLayout<personality>::EndOfLeftBorder + 304) / LineLayout<personality>::CyclesPerLine;
}
if constexpr (is_yamaha_vdp(personality)) {
@ -81,6 +85,7 @@ TMS9918<personality>::TMS9918() {
template <Personality personality>
void TMS9918<personality>::set_tv_standard(TVStandard standard) {
// TODO: the Yamaha is programmable on this at runtime.
this->tv_standard_ = standard;
switch(standard) {
case TVStandard::PAL:
@ -215,8 +220,13 @@ void TMS9918<personality>::run_for(const HalfCycles cycles) {
// Latch scrolling position, if necessary.
// ---------------------------------------
if constexpr (is_sega_vdp(personality)) {
if(this->fetch_pointer_.column < 61 && end_column >= 61) {
if(!this->fetch_pointer_.row) {
// TODO: where did this magic constant come from? https://www.smspower.org/forums/17970-RoadRashHow#111000 mentioned in passing
// that "the vertical scroll register is latched at the start of the active display" and this is two clocks before that, so it's
// not uncompelling. I can just no longer find my source.
constexpr auto latch_time = LineLayout<personality>::EndOfLeftBorder - 2;
static_assert(latch_time > 0);
if(this->fetch_pointer_.column < latch_time && end_column >= latch_time) {
Storage<personality>::latched_vertical_scroll_ = Storage<personality>::vertical_scroll_;
if(Storage<personality>::mode4_enable_) {
@ -235,7 +245,6 @@ void TMS9918<personality>::run_for(const HalfCycles cycles) {
}
// ------------------------
// Perform memory accesses.
// ------------------------

View File

@ -11,14 +11,22 @@
#include "../9918.hpp"
#include "PersonalityTraits.hpp"
#include "LineLayout.hpp"
namespace TI::TMS {
enum class Clock {
/// Whatever rate this VDP runs at, with location 0 being "the start" of the line per internal preference.
Internal,
/// A 342-cycle/line clock with the same start position as ::Internal.
TMSPixel,
/// A 171-cycle/line clock that begins at the memory window which starts straight after ::Internal = 0.
TMSMemoryWindow,
CRT
/// A fixed 1368-cycle/line clock that is used to count output to the CRT.
CRT,
/// Provides the same clock rate as ::Internal but is relocated so that 0 is the start of horizontal sync — very not coincidentally,
/// where Grauw puts 0 on his detailed TMS and Yamaha timing diagrams.
FromStartOfSync,
};
template <Personality personality, Clock clk> constexpr int clock_rate() {
@ -33,6 +41,7 @@ template <Personality personality, Clock clk> constexpr int clock_rate() {
case Clock::TMSMemoryWindow: return 171;
case Clock::CRT: return 1368;
case Clock::Internal:
case Clock::FromStartOfSync:
if constexpr (is_classic_vdp(personality)) {
return 342;
} else if constexpr (is_yamaha_vdp(personality)) {
@ -43,17 +52,41 @@ template <Personality personality, Clock clk> constexpr int clock_rate() {
}
}
template <Personality personality, Clock clock> constexpr int to_internal(int length) {
return length * clock_rate<personality, Clock::Internal>() / clock_rate<personality, clock>();
/// Statelessly converts @c length to the internal clock for @c personality; applies conversions per the list of clocks in left-to-right order.
template <Personality personality, Clock head, Clock... tail> constexpr int to_internal(int length) {
if constexpr (head == Clock::FromStartOfSync) {
length = (length + LineLayout<personality>::StartOfSync) % LineLayout<personality>::CyclesPerLine;
} else {
length = length * clock_rate<personality, Clock::Internal>() / clock_rate<personality, head>();
}
template <Personality personality, Clock clock> constexpr int from_internal(int length) {
return length * clock_rate<personality, clock>() / clock_rate<personality, Clock::Internal>();
if constexpr (!sizeof...(tail)) {
return length;
} else {
return to_internal<personality, tail...>(length);
}
}
/// Statelessly converts @c length to @c clock from the the internal clock used by VDPs of @c personality throwing away any remainder.
template <Personality personality, Clock head, Clock... tail> constexpr int from_internal(int length) {
if constexpr (head == Clock::FromStartOfSync) {
length =
(length + LineLayout<personality>::CyclesPerLine - LineLayout<personality>::StartOfSync)
% LineLayout<personality>::CyclesPerLine;
} else {
length = length * clock_rate<personality, head>() / clock_rate<personality, Clock::Internal>();
}
if constexpr (!sizeof...(tail)) {
return length;
} else {
return to_internal<personality, tail...>(length);
}
}
/*!
Provides a [potentially-]stateful conversion between the external and internal clocks.
Unlike the other clock conversions, this one may be non-integral, requiring that
Unlike the other clock conversions, this may be non-integral, requiring that
an error term be tracked.
*/
template <Personality personality> class ClockConverter {
@ -130,72 +163,6 @@ template <Personality personality> class ClockConverter {
int cycles_error_ = 0;
};
//
//
//
template <Personality personality, typename Enable = void> struct LineLayout;
// Line layout is:
//
// [0, EndOfSync] sync
// (EndOfSync, StartOfColourBurst] blank
// (StartOfColourBurst, EndOfColourBurst] colour burst
// (EndOfColourBurst, EndOfLeftErase] blank
// (EndOfLeftErase, EndOfLeftBorder] border colour
// (EndOfLeftBorder, EndOfPixels] pixel content
// (EndOfPixels, EndOfRightBorder] border colour
// [EndOfRightBorder, <end of line>] blank
//
// ... with minor caveats:
// * horizontal adjust on the Yamaha VDPs is applied to EndOfLeftBorder and EndOfPixels;
// * the Sega VDPs may programatically extend the left border; and
// * text mode on all VDPs adjusts border width.
template <Personality personality> struct LineLayout<personality, std::enable_if_t<is_classic_vdp(personality)>> {
constexpr static int EndOfSync = 26;
constexpr static int StartOfColourBurst = 29;
constexpr static int EndOfColourBurst = 43;
constexpr static int EndOfLeftErase = 50;
constexpr static int EndOfLeftBorder = 63;
constexpr static int EndOfPixels = 319;
constexpr static int EndOfRightBorder = 334;
constexpr static int CyclesPerLine = 342;
constexpr static int TextModeEndOfLeftBorder = 69;
constexpr static int TextModeEndOfPixels = 309;
constexpr static int ModeLatchCycle = 36; // Just a guess; correlates with the known 144 for the Yamaha VDPs,
// and falls into the collection gap between the final sprite
// graphics and the initial tiles or pixels.
/// The number of internal cycles that must elapse between a request to read or write and
/// it becoming a candidate for action.
constexpr static int VRAMAccessDelay = 6;
};
template <Personality personality> struct LineLayout<personality, std::enable_if_t<is_yamaha_vdp(personality)>> {
constexpr static int EndOfSync = 100;
constexpr static int StartOfColourBurst = 113;
constexpr static int EndOfColourBurst = 167;
constexpr static int EndOfLeftErase = 202;
constexpr static int EndOfLeftBorder = 258;
constexpr static int EndOfPixels = 1282;
constexpr static int EndOfRightBorder = 1341;
constexpr static int CyclesPerLine = 1368;
constexpr static int TextModeEndOfLeftBorder = 294;
constexpr static int TextModeEndOfPixels = 1254;
constexpr static int ModeLatchCycle = 144;
/// The number of internal cycles that must elapse between a request to read or write and
/// it becoming a candidate for action.
constexpr static int VRAMAccessDelay = 16;
};
}
#endif /* ClockConverter_hpp */

View File

@ -16,38 +16,19 @@ namespace TI::TMS {
1) input is a start position and an end position; they should perform the proper
operations for the period: start <= time < end.
2) times are measured relative to a 172-cycles-per-line clock (so: they directly
count access windows on the TMS and Master System).
3) within each sequencer, time 0 is the access window that straddles the beginning of
horizontal sync. Which, conveniently, is the place to which Grauw's timing diagrams
are aligned.
2) times are measured relative to the an appropriate clock they directly
count access windows on the TMS and Master System, and cycles on a Yamaha.
3) within each sequencer, cycle are numbered as per Grauw's timing diagrams. The difference
between those and internal timing, if there is one, is handled by the dispatcher.
4) all of these functions are templated with a `use_end` parameter. That will be true if
end is < 172, false otherwise. So functions can use it to eliminate should-exit-not checks,
for the more usual path of execution.
[Historically:
position 0 was the beginning of the access window immediately after the last pattern/data
block fetch that would contribute to this line, in a normal 32-column mode. So:
* it's cycle 309 on Mattias' TMS diagram;
* it's cycle 1238 on his V9938 diagram;
* it's after the last background render block in Mask of Destiny's Master System timing diagram.
That division point was selected, albeit arbitrarily, because it puts all the tile
fetches for a single line into the same [0, 171] period.
I'm moving away from this per the desire not to have V9938 output straddle two lines if horizontally-adjusted,
amongst other concerns.]
end is < [cycles per line], false otherwise. So functions can use it to eliminate
should-exit-now checks (which is likely to be the more usual path of execution).
Provided for the benefit of the methods below:
* the function external_slot(), which will perform any pending VRAM read/write.
* the macros slot(n) and external_slot(n) which can be used to schedule those things inside a
switch(start)-based implementation.
All functions should just spool data to intermediary storage. This is because for most VDPs there is
a decoupling between fetch pattern and output pattern, and it's neater to keep the same division
for the exceptions.
All functions should just spool data to intermediary storage. Fetching and drawing are decoupled.
*/
// MARK: - Address mask helpers.
@ -73,7 +54,7 @@ template<bool use_end, typename SequencerT> void Base<personality>::dispatch(Seq
#define index(n) \
if(use_end && end == n) return; \
[[fallthrough]]; \
case n: fetcher.template fetch<n>();
case n: fetcher.template fetch<from_internal<personality, Clock::FromStartOfSync>(n)>();
switch(start) {
default: assert(false);
@ -392,7 +373,7 @@ struct RefreshSequencer {
template <int cycle> void fetch() {
if(cycle < 26 || (cycle & 1) || cycle >= 154) {
base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow>(cycle));
base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow, Clock::FromStartOfSync>(cycle));
}
}
@ -406,16 +387,22 @@ struct TextSequencer {
template <int cycle> void fetch() {
// The first 30 and the final 4 slots are external.
if constexpr (cycle < 30 || cycle >= 150) {
fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow>(cycle));
fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow, Clock::FromStartOfSync>(cycle));
return;
} else {
// For the 120 slots in between follow a three-step pattern of:
constexpr int offset = cycle - 30;
constexpr auto column = AddressT(offset / 3);
switch(offset % 3) {
case 0: fetcher.fetch_name(column); break; // (1) fetch tile name.
case 1: fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow>(cycle)); break; // (2) external slot.
case 2: fetcher.fetch_pattern(column); break; // (3) fetch tile pattern.
case 0: // (1) fetch tile name.
fetcher.fetch_name(column);
break;
case 1: // (2) external slot.
fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow, Clock::FromStartOfSync>(cycle));
break;
case 2: // (3) fetch tile pattern.
fetcher.fetch_pattern(column);
break;
}
}
}
@ -432,7 +419,7 @@ struct CharacterSequencer {
template <int cycle> void fetch() {
if(cycle < 5) {
character_fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow>(cycle));
character_fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow, Clock::FromStartOfSync>(cycle));
}
if(cycle == 5) {
@ -443,7 +430,7 @@ struct CharacterSequencer {
}
if(cycle > 14 && cycle < 19) {
character_fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow>(cycle));
character_fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow, Clock::FromStartOfSync>(cycle));
}
// Fetch 8 new sprite Y coordinates, to begin selecting sprites for next line.
@ -461,7 +448,7 @@ struct CharacterSequencer {
case 0: character_fetcher.fetch_name(block); break;
case 1:
if(!(block & 3)) {
character_fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow>(cycle));
character_fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow, Clock::FromStartOfSync>(cycle));
} else {
constexpr int sprite = 8 + ((block >> 2) * 3) + ((block & 3) - 1);
sprite_fetcher.fetch_y(sprite);
@ -476,7 +463,7 @@ struct CharacterSequencer {
}
if(cycle >= 155 && cycle < 157) {
character_fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow>(cycle));
character_fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow, Clock::FromStartOfSync>(cycle));
}
if(cycle == 157) {
@ -524,7 +511,7 @@ struct SMSSequencer {
// window 0 to HSYNC low.
template <int cycle> void fetch() {
if(cycle < 3) {
fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow>(cycle));
fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow, Clock::FromStartOfSync>(cycle));
}
if(cycle == 3) {
@ -535,7 +522,7 @@ struct SMSSequencer {
}
if(cycle == 15 || cycle == 16) {
fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow>(cycle));
fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow, Clock::FromStartOfSync>(cycle));
}
if(cycle == 17) {
@ -556,7 +543,7 @@ struct SMSSequencer {
case 0: fetcher.fetch_tile_name(block); break;
case 1:
if(!(block & 3)) {
fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow>(cycle));
fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow, Clock::FromStartOfSync>(cycle));
} else {
constexpr int sprite = (8 + ((block >> 2) * 3) + ((block & 3) - 1)) << 1;
fetcher.posit_sprite(sprite);
@ -568,7 +555,7 @@ struct SMSSequencer {
}
if(cycle >= 153 && cycle < 157) {
fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow>(cycle));
fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow, Clock::FromStartOfSync>(cycle));
}
if(cycle == 157) {
@ -579,7 +566,7 @@ struct SMSSequencer {
}
if(cycle >= 169) {
fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow>(cycle));
fetcher.base->do_external_slot(to_internal<personality, Clock::TMSMemoryWindow, Clock::FromStartOfSync>(cycle));
}
}

View File

@ -0,0 +1,80 @@
//
// LineLayout.hpp
// Clock Signal
//
// Created by Thomas Harte on 18/05/2023.
// Copyright © 2023 Thomas Harte. All rights reserved.
//
#ifndef LineLayout_h
#define LineLayout_h
namespace TI::TMS {
template <Personality personality, typename Enable = void> struct LineLayout;
// Line layout is:
//
// [0, EndOfSync] sync
// (EndOfSync, StartOfColourBurst] blank
// (StartOfColourBurst, EndOfColourBurst] colour burst
// (EndOfColourBurst, EndOfLeftErase] blank
// (EndOfLeftErase, EndOfLeftBorder] border colour
// (EndOfLeftBorder, EndOfPixels] pixel content
// (EndOfPixels, EndOfRightBorder] border colour
// [EndOfRightBorder, <end of line>] blank
//
// ... with minor caveats:
// * horizontal adjust on the Yamaha VDPs is applied to EndOfLeftBorder and EndOfPixels;
// * the Sega VDPs may programatically extend the left border; and
// * text mode on all VDPs adjusts border width.
template <Personality personality> struct LineLayout<personality, std::enable_if_t<is_classic_vdp(personality)>> {
constexpr static int StartOfSync = 0;
constexpr static int EndOfSync = 26;
constexpr static int StartOfColourBurst = 29;
constexpr static int EndOfColourBurst = 43;
constexpr static int EndOfLeftErase = 50;
constexpr static int EndOfLeftBorder = 63;
constexpr static int EndOfPixels = 319;
constexpr static int EndOfRightBorder = 334;
constexpr static int CyclesPerLine = 342;
constexpr static int TextModeEndOfLeftBorder = 69;
constexpr static int TextModeEndOfPixels = 309;
constexpr static int ModeLatchCycle = 36; // Just a guess; correlates with the known 144 for the Yamaha VDPs,
// and falls into the collection gap between the final sprite
// graphics and the initial tiles or pixels.
/// The number of internal cycles that must elapse between a request to read or write and
/// it becoming a candidate for action.
constexpr static int VRAMAccessDelay = 6;
};
template <Personality personality> struct LineLayout<personality, std::enable_if_t<is_yamaha_vdp(personality)>> {
constexpr static int StartOfSync = 0;
constexpr static int EndOfSync = 100;
constexpr static int StartOfColourBurst = 113;
constexpr static int EndOfColourBurst = 167;
constexpr static int EndOfLeftErase = 202;
constexpr static int EndOfLeftBorder = 258;
constexpr static int EndOfPixels = 1282;
constexpr static int EndOfRightBorder = 1341;
constexpr static int CyclesPerLine = 1368;
constexpr static int TextModeEndOfLeftBorder = 294;
constexpr static int TextModeEndOfPixels = 1254;
constexpr static int ModeLatchCycle = 144;
/// The number of internal cycles that must elapse between a request to read or write and
/// it becoming a candidate for action.
constexpr static int VRAMAccessDelay = 16;
};
}
#endif /* LineLayout_h */

View File

@ -86,7 +86,8 @@ struct YamahaFetcher {
std::array<Event, size> result{};
size_t index = 0;
for(int c = 0; c < 1368; c++) {
const auto event = GeneratorT::event(c);
// Specific personality doesn't matter here; both Yamahas use the same internal timing.
const auto event = GeneratorT::event(from_internal<Personality::V9938, Clock::FromStartOfSync>(c));
if(!event) {
continue;
}

View File

@ -1100,6 +1100,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
428168372A16C25C008ECD27 /* LineLayout.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LineLayout.hpp; sourceTree = "<group>"; };
42AD552E2A0C4D5000ACE410 /* 68000.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = 68000.hpp; sourceTree = "<group>"; };
42AD55302A0C4D5000ACE410 /* 68000Storage.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = 68000Storage.hpp; sourceTree = "<group>"; };
42AD55312A0C4D5000ACE410 /* 68000Implementation.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = 68000Implementation.hpp; sourceTree = "<group>"; };
@ -4738,6 +4739,7 @@
4B43983F2967459B006B0BFC /* Draw.hpp */,
4B43983E29628538006B0BFC /* Fetch.hpp */,
4B2A3B5B29995FF6007CE366 /* LineBuffer.hpp */,
428168372A16C25C008ECD27 /* LineLayout.hpp */,
4B262BFF29691F55002EC0F7 /* PersonalityTraits.hpp */,
4B2A3B5A29993DFA007CE366 /* Storage.hpp */,
4BF0BC732982E54700CCA2B5 /* YamahaCommands.hpp */,