1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-11-18 17:06:15 +00:00

Merge pull request #460 from TomHarte/RWTSAcceleration

Introduces optional quick loading of Apple DOS 3.3 programs
This commit is contained in:
Thomas Harte 2018-06-10 18:41:23 -04:00 committed by GitHub
commit a1c60152d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 262 additions and 26 deletions

View File

@ -260,3 +260,7 @@ void DiskII::set_activity_observer(Activity::Observer *observer) {
drives_[0].set_activity_observer(observer, "Drive 1", true);
drives_[1].set_activity_observer(observer, "Drive 2", true);
}
Storage::Disk::Drive &DiskII::get_drive(int index) {
return drives_[index];
}

View File

@ -81,6 +81,10 @@ class DiskII:
// The Disk II functions as a potential target for @c Activity::Sources.
void set_activity_observer(Activity::Observer *observer);
// Returns the Storage::Disk::Drive in use for drive @c index.
// *NOT FOR HARDWARE EMULATION USAGE*.
Storage::Disk::Drive &get_drive(int index);
private:
enum class Control {
P0, P1, P2, P3,

View File

@ -24,20 +24,29 @@
#include "DiskIICard.hpp"
#include "Video.hpp"
#include "../../ClockReceiver/ForceInline.hpp"
#include "../../Analyser/Static/AppleII/Target.hpp"
#include "../../ClockReceiver/ForceInline.hpp"
#include "../../Configurable/Configurable.hpp"
#include "../../Storage/Disk/Track/TrackSerialiser.hpp"
#include "../../Storage/Disk/Encodings/AppleGCR/SegmentParser.hpp"
#include <algorithm>
#include <array>
#include <memory>
std::vector<std::unique_ptr<Configurable::Option>> AppleII::get_options() {
std::vector<std::unique_ptr<Configurable::Option>> options;
options.emplace_back(new Configurable::BooleanOption("Accelerate DOS 3.3", "quickload"));
return options;
}
namespace {
class ConcreteMachine:
public CRTMachine::Machine,
public ConfigurationTarget::Machine,
public KeyboardMachine::Machine,
public Configurable::Device,
public CPU::MOS6502::BusHandler,
public Inputs::Keyboard,
public AppleII::Machine,
@ -123,6 +132,10 @@ class ConcreteMachine:
pick_card_messaging_group(card);
}
AppleII::DiskIICard *diskii_card() {
return dynamic_cast<AppleII::DiskIICard *>(cards_[5].get());
}
// MARK: - Memory Map
struct MemoryBlock {
uint8_t *read_pointer = nullptr;
@ -157,6 +170,9 @@ class ConcreteMachine:
// MARK - typing
std::unique_ptr<Utility::StringSerialiser> string_serialiser_;
// MARK - quick loading
bool should_load_quickly_ = false;
public:
ConcreteMachine():
m6502_(*this),
@ -205,7 +221,7 @@ class ConcreteMachine:
return &speaker_;
}
forceinline Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) {
forceinline Cycles perform_bus_operation(const CPU::MOS6502::BusOperation operation, const uint16_t address, uint8_t *const value) {
++ cycles_since_video_update_;
++ cycles_since_card_update_;
cycles_since_audio_update_ += Cycles(7);
@ -232,21 +248,101 @@ class ConcreteMachine:
d000 to e000 : the low ROM area, which can contain indepdently-paged RAM with a language card
e000 onward : the rest of ROM, also potentially replaced with RAM by a language card
*/
uint16_t accessed_address = address;
MemoryBlock *block = nullptr;
if(address < 0x200) block = &memory_blocks_[0];
else if(address < 0xc000) {
if(address < 0x6000 && !isReadOperation(operation)) update_video();
block = &memory_blocks_[1];
address -= 0x200;
accessed_address -= 0x200;
}
else if(address < 0xd000) block = nullptr;
else if(address < 0xe000) {block = &memory_blocks_[2]; address -= 0xd000; }
else { block = &memory_blocks_[3]; address -= 0xe000; }
else if(address < 0xe000) {block = &memory_blocks_[2]; accessed_address -= 0xd000; }
else { block = &memory_blocks_[3]; accessed_address -= 0xe000; }
bool has_updated_cards = false;
if(block) {
if(isReadOperation(operation)) *value = block->read_pointer[address];
else if(block->write_pointer) block->write_pointer[address] = *value;
if(isReadOperation(operation)) *value = block->read_pointer[accessed_address];
else if(block->write_pointer) block->write_pointer[accessed_address] = *value;
if(should_load_quickly_) {
// Check for a prima facie entry into RWTS.
if(operation == CPU::MOS6502::BusOperation::ReadOpcode && address == 0xb7b5) {
// Grab the IO control block address for inspection.
uint16_t io_control_block_address =
static_cast<uint16_t>(
(m6502_.get_value_of_register(CPU::MOS6502::Register::A) << 8) |
m6502_.get_value_of_register(CPU::MOS6502::Register::Y)
);
// Verify that this is table type one, for execution on card six,
// against drive 1 or 2, and that the command is either a seek or a sector read.
if(
ram_[io_control_block_address+0x00] == 0x01 &&
ram_[io_control_block_address+0x01] == 0x60 &&
ram_[io_control_block_address+0x02] > 0 && ram_[io_control_block_address+0x02] < 3 &&
ram_[io_control_block_address+0x0c] < 2
) {
const uint8_t iob_track = ram_[io_control_block_address+4];
const uint8_t iob_sector = ram_[io_control_block_address+5];
const uint8_t iob_drive = ram_[io_control_block_address+2] - 1;
// Get the track identified and store the new head position.
auto track = diskii_card()->get_drive(iob_drive).step_to(Storage::Disk::HeadPosition(iob_track));
// DOS 3.3 keeps the current track (unspecified drive) in 0x478; the current track for drive 1 and drive 2
// is also kept in that Disk II card's screen hole.
ram_[0x478] = iob_track;
if(ram_[io_control_block_address+0x02] == 1) {
ram_[0x47e] = iob_track;
} else {
ram_[0x4fe] = iob_track;
}
// Check whether this is a read, not merely a seek.
if(ram_[io_control_block_address+0x0c] == 1) {
// Apple the DOS 3.3 formula to map the requested logical sector to a physical sector.
const int physical_sector = (iob_sector == 15) ? 15 : ((iob_sector * 13) % 15);
// Parse the entire track. TODO: cache these.
auto sector_map = Storage::Encodings::AppleGCR::sectors_from_segment(
Storage::Disk::track_serialisation(*track, Storage::Time(1, 50000)));
bool found_sector = false;
for(const auto &pair: sector_map) {
if(pair.second.address.sector == physical_sector) {
found_sector = true;
// Copy the sector contents to their destination.
uint16_t target = static_cast<uint16_t>(
ram_[io_control_block_address+8] |
(ram_[io_control_block_address+9] << 8)
);
for(size_t c = 0; c < 256; ++c) {
ram_[target] = pair.second.data[c];
++target;
}
// Set no error encountered.
ram_[io_control_block_address + 0xd] = 0;
break;
}
}
if(found_sector) {
// Set no error in the flags register too, and RTS.
m6502_.set_value_of_register(CPU::MOS6502::Register::Flags, m6502_.get_value_of_register(CPU::MOS6502::Register::Flags) & ~1);
*value = 0x60;
}
} else {
// No error encountered; RTS.
m6502_.set_value_of_register(CPU::MOS6502::Register::Flags, m6502_.get_value_of_register(CPU::MOS6502::Register::Flags) & ~1);
*value = 0x60;
}
}
}
}
} else {
// Assume a vapour read unless it turns out otherwise; this is a little
// wasteful but works for now.
@ -279,6 +375,12 @@ class ConcreteMachine:
*value = keyboard_input_;
}
break;
case 0xc061: // Switch input 0.
case 0xc062: // Switch input 1.
case 0xc063: // Switch input 2.
*value &= 0x7f;
break;
}
} else {
// Write-only switches.
@ -480,8 +582,9 @@ class ConcreteMachine:
}
bool insert_media(const Analyser::Static::Media &media) override {
if(!media.disks.empty() && cards_[5]) {
dynamic_cast<AppleII::DiskIICard *>(cards_[5].get())->set_disk(media.disks[0], 0);
if(!media.disks.empty()) {
auto diskii = diskii_card();
if(diskii) diskii->set_disk(media.disks[0], 0);
}
return true;
}
@ -492,6 +595,30 @@ class ConcreteMachine:
if(card) card->set_activity_observer(observer);
}
}
// MARK: Options
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
return AppleII::get_options();
}
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
bool quickload;
if(Configurable::get_quick_load_tape(selections_by_option, quickload)) {
should_load_quickly_ = quickload;
}
}
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet selection_set;
Configurable::append_quick_load_tape_selection(selection_set, false);
return selection_set;
}
Configurable::SelectionSet get_user_friendly_selections() override {
Configurable::SelectionSet selection_set;
Configurable::append_quick_load_tape_selection(selection_set, true);
return selection_set;
}
};
}

View File

@ -9,8 +9,16 @@
#ifndef AppleII_hpp
#define AppleII_hpp
#include "../../Configurable/Configurable.hpp"
#include <memory>
#include <vector>
namespace AppleII {
/// @returns The options available for an Apple II.
std::vector<std::unique_ptr<Configurable::Option>> get_options();
class Machine {
public:
virtual ~Machine();

View File

@ -57,3 +57,7 @@ void DiskIICard::set_component_prefers_clocking(ClockingHint::Source *component,
diskii_clocking_preference_ = preference;
set_select_constraints((preference != ClockingHint::Preference::RealTime) ? (IO | Device) : 0);
}
Storage::Disk::Drive &DiskIICard::get_drive(int drive) {
return diskii_.get_drive(drive);
}

View File

@ -32,6 +32,7 @@ class DiskIICard: public Card, public ClockingHint::Observer {
void set_activity_observer(Activity::Observer *observer) override;
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
Storage::Disk::Drive &get_drive(int drive);
private:
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override;

View File

@ -12,6 +12,7 @@
#include "../../Configurable/Configurable.hpp"
#include <cstdint>
#include <memory>
#include <vector>
namespace Electron {

View File

@ -129,6 +129,7 @@ std::string Machine::LongNameForTargetMachine(Analyser::Machine machine) {
std::map<std::string, std::vector<std::unique_ptr<Configurable::Option>>> Machine::AllOptionsByMachineName() {
std::map<std::string, std::vector<std::unique_ptr<Configurable::Option>>> options;
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::Electron), AppleII::get_options()));
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::Electron), Electron::get_options()));
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::MSX), MSX::get_options()));
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::Oric), Oric::get_options()));

View File

@ -612,6 +612,7 @@
4BC39569208EE6CF0044766B /* DiskIICard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC39566208EE6CF0044766B /* DiskIICard.cpp */; };
4BC3B74F1CD194CC00F86E85 /* Shader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC3B74D1CD194CC00F86E85 /* Shader.cpp */; };
4BC3B7521CD1956900F86E85 /* OutputShader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC3B7501CD1956900F86E85 /* OutputShader.cpp */; };
4BC5FC3020CDDDEF00410AA0 /* AppleIIOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BC5FC2E20CDDDEE00410AA0 /* AppleIIOptions.xib */; };
4BC751B21D157E61006C31D9 /* 6522Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC751B11D157E61006C31D9 /* 6522Tests.swift */; };
4BC76E691C98E31700E6EF73 /* FIRFilter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */; };
4BC76E6B1C98F43700E6EF73 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */; };
@ -1353,6 +1354,7 @@
4BC3B74E1CD194CC00F86E85 /* Shader.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Shader.hpp; sourceTree = "<group>"; };
4BC3B7501CD1956900F86E85 /* OutputShader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = OutputShader.cpp; sourceTree = "<group>"; };
4BC3B7511CD1956900F86E85 /* OutputShader.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = OutputShader.hpp; sourceTree = "<group>"; };
4BC5FC2F20CDDDEE00410AA0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/AppleIIOptions.xib"; sourceTree = SOURCE_ROOT; };
4BC751B11D157E61006C31D9 /* 6522Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 6522Tests.swift; sourceTree = "<group>"; };
4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FIRFilter.cpp; sourceTree = "<group>"; };
4BC76E681C98E31700E6EF73 /* FIRFilter.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = FIRFilter.hpp; sourceTree = "<group>"; };
@ -1986,6 +1988,7 @@
4B55CE5E1C3B7D960093A61B /* MachineDocument.swift */,
4B8FE2211DA19FB20090D3CE /* MachinePanel.swift */,
4B95FA9C1F11893B0008E395 /* ZX8081OptionsPanel.swift */,
4BC5FC2E20CDDDEE00410AA0 /* AppleIIOptions.xib */,
4B8FE2131DA19D5F0090D3CE /* Atari2600Options.xib */,
4B8FE2151DA19D5F0090D3CE /* MachineDocument.xib */,
4B2A332B1DB86821002876E3 /* OricOptions.xib */,
@ -3277,6 +3280,7 @@
4B2C45421E3C3896002A2389 /* cartridge.png in Resources */,
4BB73EA91B587A5100552FC2 /* Assets.xcassets in Resources */,
4B79E4451E3AF38600141F11 /* floppy35.png in Resources */,
4BC5FC3020CDDDEF00410AA0 /* AppleIIOptions.xib in Resources */,
4BA141BD2072E8A500A31EC9 /* MachinePicker.xib in Resources */,
4B1EDB451E39A0AC009D6819 /* chip.png in Resources */,
4B2A332D1DB86821002876E3 /* OricOptions.xib in Resources */,
@ -4036,6 +4040,14 @@
name = MainMenu.xib;
sourceTree = "<group>";
};
4BC5FC2E20CDDDEE00410AA0 /* AppleIIOptions.xib */ = {
isa = PBXVariantGroup;
children = (
4BC5FC2F20CDDDEE00410AA0 /* Base */,
);
name = AppleIIOptions.xib;
sourceTree = "<group>";
};
4BD61662206B2AC700236112 /* QuickLoadOptions.xib */ = {
isa = PBXVariantGroup;
children = (

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="MachineDocument" customModule="Clock_Signal" customModuleProvider="target">
<connections>
<outlet property="optionsPanel" destination="ZW7-Bw-4RP" id="JpE-wG-zRR"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="ZW7-Bw-4RP" customClass="MachinePanel" customModule="Clock_Signal" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES" nonactivatingPanel="YES" HUD="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="83" y="102" width="200" height="54"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/>
<view key="contentView" id="tpZ-0B-QQu">
<rect key="frame" x="0.0" y="0.0" width="200" height="54"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button translatesAutoresizingMaskIntoConstraints="NO" id="e1J-pw-zGw">
<rect key="frame" x="18" y="18" width="164" height="18"/>
<buttonCell key="cell" type="check" title="Accelerate DOS 3.3" bezelStyle="regularSquare" imagePosition="left" alignment="left" state="on" inset="2" id="tD6-UB-ESB">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="setFastLoading:" target="ZW7-Bw-4RP" id="JmG-Ks-jSh"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="e1J-pw-zGw" secondAttribute="bottom" constant="20" id="5ce-DO-a4T"/>
<constraint firstItem="e1J-pw-zGw" firstAttribute="leading" secondItem="tpZ-0B-QQu" secondAttribute="leading" constant="20" id="HSD-3d-Bl7"/>
<constraint firstAttribute="trailing" secondItem="e1J-pw-zGw" secondAttribute="trailing" constant="20" id="Q9M-FH-92N"/>
<constraint firstItem="e1J-pw-zGw" firstAttribute="top" secondItem="tpZ-0B-QQu" secondAttribute="top" constant="20" id="ul9-lf-Y3u"/>
</constraints>
</view>
<connections>
<outlet property="fastLoadingButton" destination="e1J-pw-zGw" id="jj7-OZ-mOH"/>
</connections>
<point key="canvasLocation" x="175" y="30"/>
</window>
</objects>
</document>

View File

@ -183,7 +183,7 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K
- (NSString *)optionsPanelNibName {
switch(_targets.front()->machine) {
case Analyser::Machine::AmstradCPC: return nil;
case Analyser::Machine::AppleII: return @"AppleIIOptions";
case Analyser::Machine::Atari2600: return @"Atari2600Options";
case Analyser::Machine::Electron: return @"QuickLoadCompositeOptions";
case Analyser::Machine::MSX: return @"QuickLoadCompositeOptions";

View File

@ -74,17 +74,15 @@ std::shared_ptr<Track> AppleDSK::get_track_at_position(Track::Address address) {
// In either case below, the code aims for exactly 50,000 bits per track.
if(sectors_per_track_ == 16) {
// Write gap 1.
segment += Encodings::AppleGCR::six_and_two_sync(16);
// Write the sectors.
for(uint8_t c = 0; c < 16; ++c) {
segment += Encodings::AppleGCR::six_and_two_sync(10);
segment += Encodings::AppleGCR::header(254, track, c);
segment += Encodings::AppleGCR::six_and_two_sync(10);
segment += Encodings::AppleGCR::six_and_two_sync(7); // Gap 2: 7 sync words.
segment += Encodings::AppleGCR::six_and_two_data(&track_data[logical_sector_for_physical_sector(c) * 256]);
}
// Pad if necessary.
if(segment.number_of_bits < 50000) {
segment += Encodings::AppleGCR::six_and_two_sync((50000 - segment.number_of_bits) / 10);
segment += Encodings::AppleGCR::six_and_two_sync(16); // Gap 3: 16 sync words.
}
} else {
// TODO: 5 and 3, 13-sector format. If DSK actually supports it?

View File

@ -77,6 +77,18 @@ void Drive::step(HeadPosition offset) {
}
}
std::shared_ptr<Track> Drive::step_to(HeadPosition offset) {
HeadPosition old_head_position = head_position_;
head_position_ = std::max(offset, HeadPosition(0));
if(head_position_ != old_head_position) {
track_ = nullptr;
setup_track();
}
return track_;
}
void Drive::set_head(int head) {
head = std::min(head, available_heads_ - 1);
if(head != head_) {

View File

@ -127,6 +127,17 @@ class Drive: public ClockingHint::Source, public TimedEventLoop {
/// The caller can specify whether to add an LED based on disk motor.
void set_activity_observer(Activity::Observer *observer, const std::string &name, bool add_motor_led);
/*!
Attempts to step to the specified offset and returns the track there if one exists; an uninitialised
track otherwise.
This is unambiguously **NOT A REALISTIC DRIVE FUNCTION**; real drives cannot step to a given offset.
So it is **NOT FOR HARDWARE EMULATION USAGE**.
It's for the benefit of user-optional fast-loading mechanisms **ONLY**.
*/
std::shared_ptr<Track> step_to(HeadPosition offset);
private:
// Drives contain an entire disk; from that a certain track
// will be currently under the head.

View File

@ -46,16 +46,18 @@ void PCMSegmentEventSource::reset() {
PCMSegment &PCMSegment::operator +=(const PCMSegment &rhs) {
if(!rhs.number_of_bits) return *this;
assert(((rhs.number_of_bits+7) >> 3) == rhs.data.size());
if(number_of_bits&7) {
auto target_number_of_bits = number_of_bits + rhs.number_of_bits;
data.resize((target_number_of_bits + 7) >> 3);
std::size_t first_byte = number_of_bits >> 3;
int shift = number_of_bits&7;
const int shift = number_of_bits&7;
for(std::size_t source = 0; source < rhs.data.size(); ++source) {
data[first_byte + source] |= rhs.data[source] >> shift;
if(source*8 + static_cast<std::size_t>(shift) < rhs.number_of_bits)
if(source*8 + static_cast<std::size_t>(8 - shift) < rhs.number_of_bits)
data[first_byte + source + 1] = static_cast<uint8_t>(rhs.data[source] << (8-shift));
}
@ -65,6 +67,8 @@ PCMSegment &PCMSegment::operator +=(const PCMSegment &rhs) {
number_of_bits += rhs.number_of_bits;
}
assert(((number_of_bits+7) >> 3) == data.size());
return *this;
}

View File

@ -39,22 +39,22 @@ class HeadPosition {
position_ += rhs.position_;
return *this;
}
bool operator ==(const HeadPosition &rhs) {
bool operator ==(const HeadPosition &rhs) const {
return position_ == rhs.position_;
}
bool operator !=(const HeadPosition &rhs) {
bool operator !=(const HeadPosition &rhs) const {
return position_ != rhs.position_;
}
bool operator <(const HeadPosition &rhs) {
bool operator <(const HeadPosition &rhs) const {
return position_ < rhs.position_;
}
bool operator <=(const HeadPosition &rhs) {
bool operator <=(const HeadPosition &rhs) const {
return position_ <= rhs.position_;
}
bool operator >(const HeadPosition &rhs) {
bool operator >(const HeadPosition &rhs) const {
return position_ > rhs.position_;
}
bool operator >=(const HeadPosition &rhs) {
bool operator >=(const HeadPosition &rhs) const {
return position_ >= rhs.position_;
}