2024-06-29 01:52:04 +00:00
|
|
|
//
|
|
|
|
// CPCShakerTests.m
|
|
|
|
// Clock SignalTests
|
|
|
|
//
|
|
|
|
// Created by Thomas Harte on 28/06/2024.
|
|
|
|
// Copyright © 2024 Thomas Harte. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
#import <XCTest/XCTest.h>
|
|
|
|
|
|
|
|
#include <array>
|
|
|
|
#include <cassert>
|
|
|
|
|
|
|
|
#include "CSL.hpp"
|
2024-07-01 00:19:02 +00:00
|
|
|
#include "AmstradCPC.hpp"
|
|
|
|
#include "../../../Analyser/Static/AmstradCPC/Target.hpp"
|
|
|
|
#include "../../../Machines/AmstradCPC/Keyboard.hpp"
|
2024-08-06 01:48:40 +00:00
|
|
|
#include "../../../Outputs/ScanTarget.hpp"
|
2024-07-01 00:19:02 +00:00
|
|
|
#include "CSROMFetcher.hpp"
|
|
|
|
#include "TimedMachine.hpp"
|
|
|
|
#include "MediaTarget.hpp"
|
|
|
|
#include "KeyboardMachine.hpp"
|
|
|
|
#include "MachineForTarget.hpp"
|
|
|
|
|
2024-08-06 01:48:40 +00:00
|
|
|
struct ScanTarget: public Outputs::Display::ScanTarget {
|
|
|
|
void set_modals(Modals modals) override {
|
|
|
|
modals_ = modals;
|
|
|
|
}
|
|
|
|
Scan *begin_scan() override {
|
|
|
|
return &scan_;
|
|
|
|
}
|
|
|
|
uint8_t *begin_data(size_t, size_t) override {
|
|
|
|
return data_.data();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void end_scan() override {
|
|
|
|
// Empirical, CPC-specific observation: x positions end up
|
|
|
|
// being multiplied by 61 compared to a 1:1 pixel sampling at
|
|
|
|
// the CPC's highest resolution.
|
|
|
|
const int WidthDivider = 61;
|
|
|
|
|
|
|
|
const int src_pixels = scan_.end_points[1].data_offset - scan_.end_points[0].data_offset;
|
|
|
|
const int dst_pixels = (scan_.end_points[1].x - scan_.end_points[0].x) / WidthDivider;
|
|
|
|
|
2024-08-06 02:06:23 +00:00
|
|
|
const auto x1 = scan_.end_points[0].x / WidthDivider;
|
|
|
|
const auto x2 = scan_.end_points[1].x / WidthDivider;
|
|
|
|
|
|
|
|
uint8_t *const line = &raw_image_[line_ * ImageWidth];
|
|
|
|
if(x_ < x1) {
|
|
|
|
std::fill(&line[x_], &line[x1], 0);
|
|
|
|
}
|
2024-10-16 01:15:30 +00:00
|
|
|
|
|
|
|
if(x2 != x1) {
|
|
|
|
const int step = (src_pixels << 16) / dst_pixels;
|
|
|
|
int position = 0;
|
|
|
|
|
|
|
|
for(int x = x1; x < x2; x++) {
|
|
|
|
line[x] = data_[position >> 16];
|
|
|
|
position += step;
|
|
|
|
}
|
2024-08-06 01:48:40 +00:00
|
|
|
}
|
2024-08-06 02:06:23 +00:00
|
|
|
x_ = x2;
|
2024-08-06 01:48:40 +00:00
|
|
|
}
|
|
|
|
void announce(Event event, bool, const Scan::EndPoint &, uint8_t) override {
|
|
|
|
switch(event) {
|
2024-08-06 02:06:23 +00:00
|
|
|
case Event::EndHorizontalRetrace: {
|
|
|
|
if(line_ == ImageHeight - 1) break;
|
|
|
|
|
|
|
|
if(x_ < ImageWidth) {
|
|
|
|
uint8_t *const line = &raw_image_[line_ * ImageWidth];
|
|
|
|
std::fill(&line[x_], &line[ImageWidth], 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
++line_;
|
|
|
|
x_ = 0;
|
|
|
|
} break;
|
|
|
|
case Event::EndVerticalRetrace:
|
|
|
|
std::fill(&raw_image_[line_ * ImageWidth], &raw_image_[ImageHeight * ImageWidth], 0);
|
|
|
|
line_ = 0;
|
|
|
|
x_ = 0;
|
|
|
|
break;
|
2024-08-06 01:48:40 +00:00
|
|
|
default: break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
NSBitmapImageRep *image_representation() {
|
|
|
|
NSBitmapImageRep *const result =
|
|
|
|
[[NSBitmapImageRep alloc]
|
|
|
|
initWithBitmapDataPlanes:NULL
|
|
|
|
pixelsWide:ImageWidth
|
|
|
|
pixelsHigh:ImageHeight
|
|
|
|
bitsPerSample:8
|
|
|
|
samplesPerPixel:4
|
|
|
|
hasAlpha:YES
|
|
|
|
isPlanar:NO
|
|
|
|
colorSpaceName:NSDeviceRGBColorSpace
|
|
|
|
bytesPerRow:4 * ImageWidth
|
|
|
|
bitsPerPixel:0];
|
|
|
|
uint8_t *const data = result.bitmapData;
|
|
|
|
|
|
|
|
for(int c = 0; c < ImageWidth * ImageHeight; c++) {
|
|
|
|
data[c * 4 + 0] = ((raw_image_[c] >> 4) & 3) * 127;
|
|
|
|
data[c * 4 + 1] = ((raw_image_[c] >> 2) & 3) * 127;
|
|
|
|
data[c * 4 + 2] = ((raw_image_[c] >> 0) & 3) * 127;
|
|
|
|
data[c * 4 + 3] = 0xff;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
Modals modals_;
|
|
|
|
Scan scan_;
|
|
|
|
std::array<uint8_t, 2048> data_;
|
|
|
|
int line_ = 0;
|
2024-08-06 02:06:23 +00:00
|
|
|
int x_ = 0;
|
2024-08-06 01:48:40 +00:00
|
|
|
|
|
|
|
static constexpr int ImageWidth = 914;
|
|
|
|
static constexpr int ImageHeight = 312;
|
|
|
|
std::array<uint8_t, ImageWidth*ImageHeight> raw_image_;
|
|
|
|
};
|
|
|
|
|
2024-07-01 01:26:16 +00:00
|
|
|
struct SSMDelegate: public AmstradCPC::Machine::SSMDelegate {
|
2024-08-06 01:48:40 +00:00
|
|
|
SSMDelegate(ScanTarget &scan_target) : scan_target_(scan_target) {
|
|
|
|
temp_dir_ = NSTemporaryDirectory();
|
|
|
|
NSLog(@"Outputting to %@", temp_dir_);
|
|
|
|
}
|
|
|
|
|
2024-08-08 02:00:24 +00:00
|
|
|
void set_crtc(int number) {
|
|
|
|
crtc_ = number;
|
|
|
|
}
|
|
|
|
|
2024-07-01 01:26:16 +00:00
|
|
|
void perform(uint16_t code) {
|
2024-08-08 02:00:24 +00:00
|
|
|
if(!code) {
|
|
|
|
// A code of 0000 is supposed to end a wait0000 command; at present
|
|
|
|
// there seem to be no wait0000 commands to unblock.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-08-06 01:48:40 +00:00
|
|
|
NSData *const data =
|
|
|
|
[scan_target_.image_representation() representationUsingType:NSPNGFileType properties:@{}];
|
2024-08-08 02:00:24 +00:00
|
|
|
NSString *const name = [temp_dir_ stringByAppendingPathComponent:[NSString stringWithFormat:@"CLK_%d_%04x.png", crtc_, code]];
|
2024-08-06 01:48:40 +00:00
|
|
|
[data
|
|
|
|
writeToFile:name
|
|
|
|
atomically:NO];
|
|
|
|
NSLog(@"Wrote %@", name);
|
2024-07-01 01:26:16 +00:00
|
|
|
}
|
2024-08-06 01:48:40 +00:00
|
|
|
|
|
|
|
private:
|
|
|
|
ScanTarget &scan_target_;
|
|
|
|
NSString *temp_dir_;
|
2024-08-08 02:00:24 +00:00
|
|
|
int crtc_ = 0;
|
2024-07-01 00:19:02 +00:00
|
|
|
};
|
2024-06-29 01:52:04 +00:00
|
|
|
|
2024-06-29 01:53:18 +00:00
|
|
|
//
|
|
|
|
// Runs a local capture of the test cases found at https://shaker.logonsystem.eu
|
|
|
|
//
|
2024-06-29 01:52:04 +00:00
|
|
|
@interface CPCShakerTests : XCTestCase
|
|
|
|
@end
|
|
|
|
|
2024-07-01 01:26:16 +00:00
|
|
|
@implementation CPCShakerTests {}
|
2024-06-29 01:52:04 +00:00
|
|
|
|
|
|
|
- (void)testCSLPath:(NSString *)path name:(NSString *)name {
|
|
|
|
using namespace Storage::Automation;
|
|
|
|
const auto steps = CSL::parse([[path stringByAppendingPathComponent:name] UTF8String]);
|
2024-07-01 00:19:02 +00:00
|
|
|
|
2024-08-06 01:48:40 +00:00
|
|
|
ScanTarget scan_target;
|
|
|
|
SSMDelegate ssm_delegate(scan_target);
|
2024-07-01 01:26:16 +00:00
|
|
|
|
2024-07-01 00:19:02 +00:00
|
|
|
std::unique_ptr<Machine::DynamicMachine> lazy_machine;
|
|
|
|
CSL::KeyDelay key_delay;
|
|
|
|
using Target = Analyser::Static::AmstradCPC::Target;
|
|
|
|
Target target;
|
|
|
|
target.catch_ssm_codes = true;
|
2024-07-01 01:26:16 +00:00
|
|
|
target.model = Target::Model::CPC6128;
|
2024-07-01 00:19:02 +00:00
|
|
|
|
2024-08-08 02:00:24 +00:00
|
|
|
NSString *diskPath;
|
2024-07-01 00:19:02 +00:00
|
|
|
const auto machine = [&]() -> Machine::DynamicMachine& {
|
|
|
|
if(!lazy_machine) {
|
|
|
|
Machine::Error error;
|
|
|
|
lazy_machine = Machine::MachineForTarget(&target, CSROMFetcher(), error);
|
2024-08-08 02:44:48 +00:00
|
|
|
static_cast<AmstradCPC::Machine *>(lazy_machine->raw_pointer())
|
2024-07-01 01:26:16 +00:00
|
|
|
->set_ssm_delegate(&ssm_delegate);
|
2024-08-06 01:48:40 +00:00
|
|
|
lazy_machine->scan_producer()->set_scan_target(&scan_target);
|
2024-08-08 02:00:24 +00:00
|
|
|
|
|
|
|
if(diskPath) {
|
|
|
|
const auto media = Analyser::Static::GetMedia(diskPath.UTF8String);
|
|
|
|
lazy_machine->media_target()->insert_media(media);
|
|
|
|
}
|
2024-07-01 00:19:02 +00:00
|
|
|
}
|
|
|
|
return *lazy_machine;
|
|
|
|
};
|
|
|
|
const auto delay = [&](uint64_t micros) {
|
|
|
|
machine().timed_machine()->run_for((double)micros / 1'000'000.0);
|
|
|
|
};
|
|
|
|
|
|
|
|
using Type = CSL::Instruction::Type;
|
|
|
|
for(const auto &step: steps) {
|
|
|
|
switch(step.type) {
|
|
|
|
case Type::Version:
|
|
|
|
if(std::get<std::string>(step.argument) != "1.0") {
|
|
|
|
XCTAssert(false, "Unrecognised file version");
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Type::CRTCSelect: {
|
2024-08-08 02:00:24 +00:00
|
|
|
const auto argument = static_cast<int>(std::get<uint64_t>(step.argument));
|
|
|
|
switch(argument) {
|
|
|
|
default:
|
|
|
|
NSLog(@"Unrecognised CRTC type %d", argument);
|
|
|
|
break;
|
2024-07-01 00:19:02 +00:00
|
|
|
case 0: target.crtc_type = Target::CRTCType::Type0; break;
|
|
|
|
case 1: target.crtc_type = Target::CRTCType::Type1; break;
|
|
|
|
case 2: target.crtc_type = Target::CRTCType::Type2; break;
|
|
|
|
case 3: target.crtc_type = Target::CRTCType::Type3; break;
|
|
|
|
}
|
2024-08-08 02:00:24 +00:00
|
|
|
ssm_delegate.set_crtc(argument);
|
2024-07-01 00:19:02 +00:00
|
|
|
} break;
|
|
|
|
|
|
|
|
case Type::Reset:
|
2024-08-08 02:00:24 +00:00
|
|
|
lazy_machine.reset();
|
2024-07-01 00:19:02 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
case Type::Wait:
|
|
|
|
delay(std::get<uint64_t>(step.argument));
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Type::DiskInsert: {
|
|
|
|
const auto &disk = std::get<CSL::DiskInsert>(step.argument);
|
|
|
|
XCTAssertEqual(disk.drive, 0); // Only drive 0 is supported for now.
|
|
|
|
|
|
|
|
NSString *diskName = [NSString stringWithUTF8String:disk.file.c_str()];
|
2024-08-08 02:00:24 +00:00
|
|
|
diskPath =
|
2024-07-01 00:19:02 +00:00
|
|
|
[[NSBundle bundleForClass:[self class]]
|
|
|
|
pathForResource:diskName ofType:nil inDirectory:@"Shaker"];
|
|
|
|
XCTAssertNotNil(diskPath);
|
|
|
|
|
2024-08-08 02:00:24 +00:00
|
|
|
if(lazy_machine) {
|
|
|
|
const auto media = Analyser::Static::GetMedia(diskPath.UTF8String);
|
|
|
|
machine().media_target()->insert_media(media);
|
|
|
|
}
|
2024-07-01 00:19:02 +00:00
|
|
|
} break;
|
|
|
|
|
|
|
|
case Type::KeyDelay:
|
|
|
|
key_delay = std::get<CSL::KeyDelay>(step.argument);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Type::KeyOutput: {
|
|
|
|
auto &key_target = *machine().keyboard_machine();
|
|
|
|
|
|
|
|
const auto &events = std::get<std::vector<CSL::KeyEvent>>(step.argument);
|
|
|
|
bool last_down = false;
|
|
|
|
for(const auto &event: events) {
|
|
|
|
// Apply the interpress delay before if this is a second consecutive press;
|
|
|
|
// if this is a release then apply the regular key delay.
|
|
|
|
if(event.down && !last_down) {
|
|
|
|
delay(key_delay.interpress_delay);
|
|
|
|
} else if(!event.down) {
|
|
|
|
delay(key_delay.press_delay);
|
|
|
|
}
|
|
|
|
|
|
|
|
key_target.set_key_state(event.key, event.down);
|
|
|
|
last_down = event.down;
|
|
|
|
|
|
|
|
// If this was the release of a carriage return, wait some more after release.
|
|
|
|
if(key_delay.carriage_return_delay && (event.key == AmstradCPC::Key::KeyEnter || event.key == AmstradCPC::Key::KeyReturn)) {
|
|
|
|
delay(*key_delay.carriage_return_delay);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} break;
|
|
|
|
|
|
|
|
case Type::LoadCSL:
|
|
|
|
// Quick fix: just recurse.
|
|
|
|
[self
|
|
|
|
testCSLPath:path
|
|
|
|
name:
|
|
|
|
[NSString stringWithUTF8String:
|
|
|
|
(std::get<std::string>(step.argument) + ".csl").c_str()
|
|
|
|
]];
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
XCTAssert(false, "Unrecognised command: %d", step.type);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2024-06-29 01:52:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)testModulePath:(NSString *)path name:(NSString *)name {
|
|
|
|
NSString *basePath =
|
|
|
|
[[NSBundle bundleForClass:[self class]]
|
|
|
|
pathForResource:@"Shaker"
|
|
|
|
ofType:nil];
|
|
|
|
[self testCSLPath:[basePath stringByAppendingPathComponent:path] name:name];
|
|
|
|
}
|
|
|
|
|
2024-08-08 02:00:24 +00:00
|
|
|
- (void)testModuleA {
|
|
|
|
[self testModulePath:@"MODULE A" name:@"SHAKE26A-0.CSL"];
|
2024-08-08 02:44:48 +00:00
|
|
|
// [self testModulePath:@"MODULE A" name:@"SHAKE26A-1.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE A" name:@"SHAKE26A-2.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE A" name:@"SHAKE26A-3.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE A" name:@"SHAKE26A-4.CSL"];
|
2024-08-08 02:00:24 +00:00
|
|
|
}
|
|
|
|
- (void)testModuleB {
|
|
|
|
[self testModulePath:@"MODULE B" name:@"SHAKE26B-0.CSL"];
|
2024-08-08 02:44:48 +00:00
|
|
|
// [self testModulePath:@"MODULE B" name:@"SHAKE26B-1.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE B" name:@"SHAKE26B-2.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE B" name:@"SHAKE26B-3.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE B" name:@"SHAKE26B-4.CSL"];
|
2024-08-08 02:00:24 +00:00
|
|
|
}
|
|
|
|
- (void)testModuleC {
|
|
|
|
[self testModulePath:@"MODULE C" name:@"SHAKE26C-0.CSL"];
|
2024-08-08 02:44:48 +00:00
|
|
|
// [self testModulePath:@"MODULE C" name:@"SHAKE26C-1.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE C" name:@"SHAKE26C-2.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE C" name:@"SHAKE26C-3.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE C" name:@"SHAKE26C-4.CSL"];
|
2024-08-08 02:00:24 +00:00
|
|
|
}
|
|
|
|
- (void)testModuleD {
|
|
|
|
[self testModulePath:@"MODULE D" name:@"SHAKE26D-0.CSL"];
|
2024-08-08 02:44:48 +00:00
|
|
|
// [self testModulePath:@"MODULE D" name:@"SHAKE26D-1.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE D" name:@"SHAKE26D-2.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE D" name:@"SHAKE26D-3.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE D" name:@"SHAKE26D-4.CSL"];
|
2024-08-08 02:00:24 +00:00
|
|
|
}
|
|
|
|
- (void)testModuleE {
|
|
|
|
[self testModulePath:@"MODULE E" name:@"SHAKE26E-0.CSL"];
|
2024-08-08 02:44:48 +00:00
|
|
|
// [self testModulePath:@"MODULE E" name:@"SHAKE26E-1.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE E" name:@"SHAKE26E-2.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE E" name:@"SHAKE26E-3.CSL"];
|
|
|
|
// [self testModulePath:@"MODULE E" name:@"SHAKE26E-4.CSL"];
|
2024-08-08 02:00:24 +00:00
|
|
|
}
|
2024-06-29 01:52:04 +00:00
|
|
|
|
|
|
|
@end
|