mirror of
https://github.com/TomHarte/CLK.git
synced 2026-01-23 16:16:16 +00:00
319 lines
8.5 KiB
C++
319 lines
8.5 KiB
C++
//
|
|
// LinearMemory.hpp
|
|
// Clock Signal
|
|
//
|
|
// Created by Thomas Harte on 24/03/2025.
|
|
// Copyright © 2025 Thomas Harte. All rights reserved.
|
|
//
|
|
|
|
#pragma once
|
|
|
|
#include "InstructionSets/x86/AccessType.hpp"
|
|
#include "InstructionSets/x86/Model.hpp"
|
|
#include "Machines/Utility/MemoryFuzzer.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <cstdint>
|
|
#include <utility>
|
|
|
|
namespace PCCompatible {
|
|
|
|
// TODO: send writes to the ROM area off to nowhere.
|
|
// TODO: support banked sections for EGA/VGA and possibly EMS purposes.
|
|
|
|
struct DummyValue {
|
|
public:
|
|
template <typename IntT>
|
|
IntT &value() {
|
|
if constexpr (std::is_same_v<IntT, uint32_t>) {
|
|
return dummies_.dummy32;
|
|
} else if constexpr (std::is_same_v<IntT, uint16_t>) {
|
|
return dummies_.dummy16;
|
|
} else if constexpr (std::is_same_v<IntT, uint8_t>) {
|
|
return dummies_.dummy8;
|
|
}
|
|
}
|
|
|
|
private:
|
|
union {
|
|
uint32_t dummy32;
|
|
uint16_t dummy16;
|
|
uint8_t dummy8;
|
|
} dummies_;
|
|
};
|
|
|
|
template <size_t MaxAddressV>
|
|
struct LinearPool {
|
|
static constexpr size_t MaxAddress = MaxAddressV;
|
|
|
|
LinearPool() {
|
|
Memory::Fuzz(memory);
|
|
}
|
|
|
|
//
|
|
// External access.
|
|
//
|
|
|
|
// Provided for setup.
|
|
void install(const uint32_t address, const uint8_t *const data, const uint32_t length) {
|
|
std::copy(data, data + length, memory.begin() + std::vector<uint8_t>::difference_type(address));
|
|
}
|
|
|
|
// Used by both DMA devices and by the CGA and MDA cards to set up their base pointers.
|
|
// @c address is always physical.
|
|
uint8_t *at(const uint32_t address) {
|
|
return &memory[address];
|
|
}
|
|
|
|
template <typename IntT>
|
|
IntT read(const uint32_t address) {
|
|
if(address >= MaxAddressV) return IntT(~0);
|
|
return *reinterpret_cast<IntT *>(&memory[address]);
|
|
}
|
|
|
|
protected:
|
|
std::array<uint8_t, MaxAddress> memory;
|
|
};
|
|
|
|
struct SplitHolder {
|
|
using AccessType = InstructionSet::x86::AccessType;
|
|
|
|
template <typename IntT, AccessType type>
|
|
typename InstructionSet::x86::Accessor<IntT, type>::type access(
|
|
const uint32_t address,
|
|
const uint32_t base,
|
|
const uint32_t bytes_available,
|
|
uint8_t *const memory
|
|
) {
|
|
if(bytes_available >= sizeof(IntT)) {
|
|
return *reinterpret_cast<IntT *>(&memory[address]);
|
|
}
|
|
|
|
// This is a large quantity that straddles the limit,
|
|
// but if it's being read only then just assemble it and
|
|
// forget about things...
|
|
if constexpr (!is_writeable(type)) {
|
|
if constexpr (std::is_same_v<IntT, uint16_t>) {
|
|
return uint16_t(memory[address] | (memory[base] << 8));
|
|
}
|
|
|
|
IntT result;
|
|
auto buffer = reinterpret_cast<uint8_t *>(&result);
|
|
std::memcpy(buffer, &memory[address], bytes_available);
|
|
std::memcpy(buffer + bytes_available, &memory[base], sizeof(IntT) - bytes_available);
|
|
return result;
|
|
}
|
|
|
|
// The caller needs an atomic unit that looks like an IntT and will
|
|
// need to be written out eventually, so set up for that.
|
|
write_back_address_[0] = address;
|
|
write_back_address_[1] = base;
|
|
write_back_lead_size_ = bytes_available;
|
|
|
|
// Seed value only if this is a modify
|
|
if constexpr (type == AccessType::ReadModifyWrite) {
|
|
auto buffer = reinterpret_cast<uint8_t *>(write_back_value<IntT>());
|
|
if constexpr (std::is_same_v<IntT, uint16_t>) {
|
|
buffer[0] = memory[address];
|
|
buffer[1] = memory[base];
|
|
} else {
|
|
std::memcpy(buffer, &memory[address], bytes_available);
|
|
std::memcpy(buffer + bytes_available, &memory[base], sizeof(IntT) - bytes_available);
|
|
}
|
|
}
|
|
|
|
return *write_back_value<IntT>();
|
|
}
|
|
|
|
template <typename IntT>
|
|
void write_back(uint8_t *const memory) {
|
|
if constexpr (std::is_same_v<IntT, uint8_t>) {
|
|
return;
|
|
}
|
|
|
|
if(write_back_address_[0] == NoWriteBack) {
|
|
return;
|
|
}
|
|
|
|
auto buffer = reinterpret_cast<const uint8_t *>(write_back_value<IntT>());
|
|
if constexpr (std::is_same_v<IntT, uint16_t>) {
|
|
memory[write_back_address_[0]] = buffer[0];
|
|
memory[write_back_address_[1]] = buffer[1];
|
|
} else {
|
|
std::memcpy(&memory[write_back_address_[0]], buffer, write_back_lead_size_);
|
|
std::memcpy(&memory[write_back_address_[1]], buffer + write_back_lead_size_, sizeof(IntT) - write_back_lead_size_);
|
|
}
|
|
write_back_address_[0] = NoWriteBack;
|
|
}
|
|
|
|
private:
|
|
static constexpr uint32_t NoWriteBack = 0; // A low byte address of 0 can't require write-back.
|
|
uint32_t write_back_address_[2] = {NoWriteBack, NoWriteBack};
|
|
uint32_t write_back_lead_size_;
|
|
union {
|
|
uint16_t word;
|
|
uint32_t dword;
|
|
} write_back_value_;
|
|
template <typename IntT> IntT *write_back_value() {
|
|
static_assert(std::is_same_v<IntT, uint16_t> || std::is_same_v<IntT, uint32_t>);
|
|
if constexpr (std::is_same_v<IntT, uint16_t>) {
|
|
return &write_back_value_.word;
|
|
} else {
|
|
return &write_back_value_.dword;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
template <InstructionSet::x86::Model model, typename Enable = void> struct LinearMemory;
|
|
|
|
template <InstructionSet::x86::Model model>
|
|
struct LinearMemory<model, std::enable_if_t<model <= InstructionSet::x86::Model::i80186>>:
|
|
public SplitHolder,
|
|
public LinearPool<1 << 20>
|
|
{
|
|
static constexpr bool RequiresPreauthorisation = false;
|
|
|
|
template <typename IntT, AccessType type>
|
|
typename InstructionSet::x86::Accessor<IntT, type>::type access(
|
|
uint32_t address,
|
|
const uint32_t base
|
|
) {
|
|
address &= MaxAddress - 1;
|
|
|
|
// Bytes: always safe.
|
|
if constexpr (std::is_same_v<IntT, uint8_t>) {
|
|
return memory[address];
|
|
} else {
|
|
// Split on end of address space.
|
|
if(address == MaxAddress - 1) {
|
|
return SplitHolder::access<IntT, type>(address, base, 1, memory.data());
|
|
}
|
|
|
|
// Split on end of segment if this is an 8086.
|
|
if constexpr (model == InstructionSet::x86::Model::i8086) {
|
|
const uint32_t offset = address - base;
|
|
if(offset == 0xffff) {
|
|
return SplitHolder::access<IntT, type>(address, base, 1, memory.data());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Don't split.
|
|
return *reinterpret_cast<IntT *>(&memory[address]);
|
|
}
|
|
|
|
template <typename IntT>
|
|
void write_back() {
|
|
if constexpr (!std::is_same_v<IntT, uint8_t>) {
|
|
SplitHolder::write_back<IntT>(memory.data());
|
|
}
|
|
}
|
|
|
|
template <typename IntT>
|
|
void preauthorised_write(
|
|
uint32_t address,
|
|
const uint32_t base,
|
|
IntT value
|
|
) {
|
|
address &= MaxAddress - 1;
|
|
|
|
// Bytes can be written without further ado.
|
|
if constexpr (std::is_same_v<IntT, uint8_t>) {
|
|
memory[address] = value;
|
|
return;
|
|
}
|
|
|
|
// Words that straddle the segment end must be split in two.
|
|
if constexpr (model == InstructionSet::x86::Model::i8086) {
|
|
const uint32_t offset = address - base;
|
|
if(offset == 0xffff) {
|
|
memory[address] = uint8_t(value & 0xff);
|
|
memory[base] = uint8_t(value >> 8);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Words that straddle the end of physical RAM must also be split in two.
|
|
if(address == MaxAddress - 1) {
|
|
memory[address] = uint8_t(value & 0xff);
|
|
memory[0] = uint8_t(value >> 8);
|
|
return;
|
|
}
|
|
|
|
// It's safe just to write then.
|
|
*reinterpret_cast<IntT *>(&memory[address]) = value;
|
|
}
|
|
|
|
void preauthorise_read(uint32_t, uint32_t) {}
|
|
void preauthorise_write(uint32_t, uint32_t) {}
|
|
};
|
|
|
|
// TODO: increase template parameter to LinearPool, which is the base RAM size.
|
|
template <>
|
|
struct LinearMemory<InstructionSet::x86::Model::i80286>: public LinearPool<1 << 20> {
|
|
static constexpr bool RequiresPreauthorisation = true;
|
|
|
|
LinearMemory() {
|
|
set_a20_enabled(true);
|
|
}
|
|
|
|
// A20 is the only thing that can cause split accesses on an 80286.
|
|
void set_a20_enabled(const bool enabled) {
|
|
address_mask_ = uint32_t(~0);
|
|
if(!enabled) {
|
|
address_mask_ &= uint32_t(~(1 << 20));
|
|
}
|
|
}
|
|
|
|
using AccessType = InstructionSet::x86::AccessType;
|
|
|
|
template <typename IntT, AccessType type>
|
|
typename InstructionSet::x86::Accessor<IntT, type>::type access(
|
|
uint32_t address, uint32_t
|
|
) {
|
|
if(MaxAddress != (1 << 24) && (address & address_mask_) >= MaxAddress) {
|
|
return dummy_.value<IntT>();
|
|
}
|
|
return *reinterpret_cast<IntT *>(&memory[address & address_mask_]);
|
|
}
|
|
|
|
template <typename IntT, AccessType type>
|
|
typename InstructionSet::x86::Accessor<IntT, type>::type access(
|
|
uint32_t address, uint32_t
|
|
) const {
|
|
static_assert(!is_writeable(type));
|
|
if(MaxAddress != (1 << 24) && (address & address_mask_) >= MaxAddress) {
|
|
return dummy_.value<IntT>();
|
|
}
|
|
return *reinterpret_cast<const IntT *>(&memory[address & address_mask_]);
|
|
}
|
|
|
|
template <typename IntT>
|
|
void write_back() {
|
|
dummy_.value<IntT>() = IntT(~0);
|
|
}
|
|
|
|
template <typename IntT>
|
|
void preauthorised_write(
|
|
uint32_t address,
|
|
const uint32_t,
|
|
IntT value
|
|
) {
|
|
if(MaxAddress != (1 << 24) && (address & address_mask_) >= MaxAddress) return;
|
|
*reinterpret_cast<IntT *>(&memory[address & address_mask_]) = value;
|
|
}
|
|
|
|
void preauthorise_read(uint32_t, uint32_t) {}
|
|
void preauthorise_write(uint32_t, uint32_t) {}
|
|
|
|
private:
|
|
uint32_t address_mask_;
|
|
inline static DummyValue dummy_;
|
|
};
|
|
|
|
}
|