2016-09-06 10:39:40 +00:00
|
|
|
//
|
|
|
|
// CommodoreAnalyser.cpp
|
|
|
|
// Clock Signal
|
|
|
|
//
|
|
|
|
// Created by Thomas Harte on 06/09/2016.
|
2018-05-13 19:19:52 +00:00
|
|
|
// Copyright 2016 Thomas Harte. All rights reserved.
|
2016-09-06 10:39:40 +00:00
|
|
|
//
|
|
|
|
|
2016-09-15 23:24:59 +00:00
|
|
|
#include "StaticAnalyser.hpp"
|
2016-09-06 10:39:40 +00:00
|
|
|
|
2018-01-25 02:48:44 +00:00
|
|
|
#include "Disk.hpp"
|
2016-09-13 11:26:51 +00:00
|
|
|
#include "File.hpp"
|
2016-09-06 10:39:40 +00:00
|
|
|
#include "Tape.hpp"
|
2018-03-09 21:07:29 +00:00
|
|
|
#include "Target.hpp"
|
2018-01-25 02:48:44 +00:00
|
|
|
#include "../../../Storage/Cartridge/Encodings/CommodoreROM.hpp"
|
2019-03-02 23:07:05 +00:00
|
|
|
#include "../../../Outputs/Log.hpp"
|
2016-09-06 10:39:40 +00:00
|
|
|
|
2024-12-05 02:41:05 +00:00
|
|
|
#include "../Disassembler/6502.hpp"
|
|
|
|
#include "../Disassembler/AddressMapper.hpp"
|
|
|
|
|
2018-04-07 00:07:10 +00:00
|
|
|
#include <algorithm>
|
2019-12-27 03:53:09 +00:00
|
|
|
#include <cstring>
|
2017-05-06 23:55:42 +00:00
|
|
|
#include <sstream>
|
2024-12-05 22:30:30 +00:00
|
|
|
#include <unordered_set>
|
2017-05-06 23:55:42 +00:00
|
|
|
|
2018-01-25 02:48:44 +00:00
|
|
|
using namespace Analyser::Static::Commodore;
|
2016-09-06 10:39:40 +00:00
|
|
|
|
2024-12-05 02:41:05 +00:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
2024-11-30 02:08:35 +00:00
|
|
|
Vic20CartridgesFrom(const std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges) {
|
2018-01-24 03:18:16 +00:00
|
|
|
std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> vic20_cartridges;
|
2016-09-29 23:39:13 +00:00
|
|
|
|
2017-12-02 21:01:30 +00:00
|
|
|
for(const auto &cartridge : cartridges) {
|
|
|
|
const auto &segments = cartridge->get_segments();
|
2016-09-29 23:39:13 +00:00
|
|
|
|
2024-12-05 02:41:05 +00:00
|
|
|
// Only one mapped item is allowed ...
|
2016-09-29 23:39:13 +00:00
|
|
|
if(segments.size() != 1) continue;
|
|
|
|
|
2024-12-05 02:41:05 +00:00
|
|
|
// ... which must be 16 kb in size.
|
2016-09-29 23:39:13 +00:00
|
|
|
Storage::Cartridge::Cartridge::Segment segment = segments.front();
|
|
|
|
if(segment.start_address != 0xa000) continue;
|
|
|
|
if(!Storage::Cartridge::Encodings::CommodoreROM::isROM(segment.data)) continue;
|
|
|
|
|
|
|
|
vic20_cartridges.push_back(cartridge);
|
|
|
|
}
|
|
|
|
|
2024-12-05 02:41:05 +00:00
|
|
|
// TODO: other machines?
|
|
|
|
|
2016-09-29 23:39:13 +00:00
|
|
|
return vic20_cartridges;
|
|
|
|
}
|
|
|
|
|
2024-12-05 02:41:05 +00:00
|
|
|
struct BASICAnalysis {
|
|
|
|
enum class Version {
|
|
|
|
BASIC2,
|
|
|
|
BASIC4,
|
|
|
|
BASIC3_5,
|
|
|
|
} minimum_version = Version::BASIC2;
|
|
|
|
std::vector<uint16_t> machine_code_addresses;
|
|
|
|
};
|
|
|
|
|
|
|
|
std::optional<BASICAnalysis> analyse(const File &file) {
|
|
|
|
// Accept only 'program' types.
|
|
|
|
if(file.type != File::RelocatableProgram && file.type != File::NonRelocatableProgram) {
|
|
|
|
return std::nullopt;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint16_t line_address = file.starting_address;
|
|
|
|
int previous_line_number = -1;
|
|
|
|
|
|
|
|
const auto byte = [&](uint16_t address) {
|
|
|
|
return file.data[address - file.starting_address];
|
|
|
|
};
|
|
|
|
const auto word = [&](uint16_t address) {
|
|
|
|
return uint16_t(byte(address) | byte(address + 1) << 8);
|
|
|
|
};
|
|
|
|
|
|
|
|
// BASIC programs have a per-line structure of:
|
|
|
|
// [2 bytes: address of start of next line]
|
|
|
|
// [2 bytes: this line number]
|
|
|
|
// ... null-terminated code ...
|
|
|
|
// (with a next line address of 0000 indicating end of program)
|
|
|
|
//
|
|
|
|
// If a SYS is encountered that jumps into the BASIC program then treat that as
|
|
|
|
// a machine code entry point.
|
|
|
|
|
|
|
|
BASICAnalysis analysis;
|
2024-12-05 22:30:30 +00:00
|
|
|
std::unordered_set<uint16_t> visited_lines;
|
2024-12-05 02:41:05 +00:00
|
|
|
while(true) {
|
|
|
|
// Analysis has failed if there isn't at least one complete BASIC line from here.
|
|
|
|
if(size_t(line_address - file.starting_address) + 5 >= file.data.size()) {
|
|
|
|
return std::nullopt;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto next_line_address = word(line_address);
|
|
|
|
const auto line_number = word(line_address + 2);
|
|
|
|
|
|
|
|
uint16_t code = line_address + 4;
|
|
|
|
const auto next = [&]() -> uint8_t {
|
|
|
|
if(code >= file.starting_address + file.data.size()) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return byte(code++);
|
|
|
|
};
|
|
|
|
|
|
|
|
while(true) {
|
|
|
|
const auto token = next();
|
|
|
|
if(!token) break;
|
|
|
|
|
|
|
|
switch(token) {
|
|
|
|
case 0x9e: { // SYS; parse following ASCII argument.
|
|
|
|
uint16_t address = 0;
|
|
|
|
while(true) {
|
|
|
|
const auto c = next();
|
|
|
|
if(c < '0' || c > '9') {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
address = (address * 10) + (c - '0');
|
|
|
|
};
|
|
|
|
analysis.machine_code_addresses.push_back(address);
|
|
|
|
} break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-05 22:28:47 +00:00
|
|
|
// Exit if a formal end of the program has been declared or if, as some copy protections do,
|
|
|
|
// the linked list of line contents has been made circular.
|
2024-12-05 22:30:30 +00:00
|
|
|
visited_lines.insert(line_address);
|
2024-12-05 22:28:47 +00:00
|
|
|
if(!next_line_address || visited_lines.find(next_line_address) != visited_lines.end()) {
|
2024-12-05 02:41:05 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
previous_line_number = line_number;
|
|
|
|
line_address = next_line_address;
|
|
|
|
}
|
|
|
|
|
|
|
|
return analysis;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2024-11-30 02:08:35 +00:00
|
|
|
Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(
|
|
|
|
const Media &media,
|
|
|
|
const std::string &file_name,
|
|
|
|
TargetPlatform::IntType
|
|
|
|
) {
|
2018-04-14 23:46:15 +00:00
|
|
|
TargetList destination;
|
2019-12-22 04:52:04 +00:00
|
|
|
auto target = std::make_unique<Target>();
|
2016-09-06 10:39:40 +00:00
|
|
|
|
2016-09-13 11:26:51 +00:00
|
|
|
int device = 0;
|
2018-01-24 03:18:16 +00:00
|
|
|
std::vector<File> files;
|
2016-09-15 23:21:09 +00:00
|
|
|
bool is_disk = false;
|
2016-09-13 11:26:51 +00:00
|
|
|
|
2024-12-05 02:41:05 +00:00
|
|
|
// Strip out inappropriate cartridges.
|
2018-01-25 03:35:54 +00:00
|
|
|
target->media.cartridges = Vic20CartridgesFrom(media.cartridges);
|
2016-09-06 10:39:40 +00:00
|
|
|
|
2024-12-05 02:41:05 +00:00
|
|
|
// Find all valid Commodore files on disks.
|
2017-08-17 14:48:29 +00:00
|
|
|
for(auto &disk : media.disks) {
|
2018-01-24 03:18:16 +00:00
|
|
|
std::vector<File> disk_files = GetFiles(disk);
|
2017-09-05 00:54:38 +00:00
|
|
|
if(!disk_files.empty()) {
|
2016-09-15 23:21:09 +00:00
|
|
|
is_disk = true;
|
2018-01-24 03:18:16 +00:00
|
|
|
files.insert(files.end(), disk_files.begin(), disk_files.end());
|
2018-01-25 03:35:54 +00:00
|
|
|
target->media.disks.push_back(disk);
|
2016-09-13 11:26:51 +00:00
|
|
|
if(!device) device = 8;
|
|
|
|
}
|
|
|
|
}
|
2016-09-07 11:39:47 +00:00
|
|
|
|
2024-12-05 02:41:05 +00:00
|
|
|
// Find all valid Commodore files on tapes.
|
2017-08-17 14:48:29 +00:00
|
|
|
for(auto &tape : media.tapes) {
|
2018-01-24 03:18:16 +00:00
|
|
|
std::vector<File> tape_files = GetFiles(tape);
|
2017-07-13 01:34:08 +00:00
|
|
|
tape->reset();
|
2017-09-05 00:54:38 +00:00
|
|
|
if(!tape_files.empty()) {
|
2018-01-24 03:18:16 +00:00
|
|
|
files.insert(files.end(), tape_files.begin(), tape_files.end());
|
2018-01-25 03:35:54 +00:00
|
|
|
target->media.tapes.push_back(tape);
|
2016-09-13 11:26:51 +00:00
|
|
|
if(!device) device = 1;
|
|
|
|
}
|
|
|
|
}
|
2016-09-08 02:17:19 +00:00
|
|
|
|
2024-12-05 02:41:05 +00:00
|
|
|
// Inspect discovered files to try to divine machine and memory model.
|
2017-09-05 00:54:38 +00:00
|
|
|
if(!files.empty()) {
|
2024-12-05 02:41:05 +00:00
|
|
|
const auto &file = files.front();
|
|
|
|
|
2019-12-27 03:49:48 +00:00
|
|
|
auto memory_model = Target::MemoryModel::Unexpanded;
|
2017-05-06 23:55:42 +00:00
|
|
|
std::ostringstream string_stream;
|
|
|
|
string_stream << "LOAD\"" << (is_disk ? "*" : "") << "\"," << device << ",";
|
2024-12-05 02:41:05 +00:00
|
|
|
|
|
|
|
const auto analysis = analyse(file);
|
2024-12-05 03:04:00 +00:00
|
|
|
if(analysis && !analysis->machine_code_addresses.empty()) {
|
2017-05-06 23:55:42 +00:00
|
|
|
string_stream << "1";
|
2024-12-05 02:41:05 +00:00
|
|
|
|
2024-12-05 22:28:47 +00:00
|
|
|
// Disassemble.
|
2024-12-05 02:41:05 +00:00
|
|
|
const auto disassembly = Analyser::Static::MOS6502::Disassemble(
|
|
|
|
file.data,
|
|
|
|
Analyser::Static::Disassembler::OffsetMapper(file.starting_address),
|
|
|
|
analysis->machine_code_addresses
|
|
|
|
);
|
|
|
|
|
2024-12-05 22:28:47 +00:00
|
|
|
// Very dumb check: if FF3E or FF3F were touched, this is for the +4.
|
|
|
|
for(const auto address: {0xff3e, 0xff3f}) {
|
|
|
|
for(const auto &collection: {
|
|
|
|
disassembly.external_loads,
|
|
|
|
disassembly.external_stores,
|
|
|
|
disassembly.external_modifies
|
|
|
|
}) {
|
|
|
|
if(collection.find(uint16_t(address)) != collection.end()) {
|
|
|
|
target->machine = Machine::Plus4; // TODO: use a better target?
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-09-13 11:26:51 +00:00
|
|
|
}
|
2024-12-05 02:41:05 +00:00
|
|
|
|
2017-05-06 23:55:42 +00:00
|
|
|
string_stream << "\nRUN\n";
|
2018-01-25 03:35:54 +00:00
|
|
|
target->loading_command = string_stream.str();
|
2016-09-13 11:26:51 +00:00
|
|
|
|
|
|
|
// make a first guess based on loading address
|
2017-03-26 18:34:47 +00:00
|
|
|
switch(files.front().starting_address) {
|
2018-04-01 00:58:16 +00:00
|
|
|
default:
|
2024-11-30 02:08:35 +00:00
|
|
|
Log::Logger<Log::Source::CommodoreStaticAnalyser>().error().append(
|
|
|
|
"Unrecognised loading address for Commodore program: %04x", files.front().starting_address);
|
2020-06-20 03:36:51 +00:00
|
|
|
[[fallthrough]];
|
2016-09-13 11:26:51 +00:00
|
|
|
case 0x1001:
|
2019-12-27 03:49:48 +00:00
|
|
|
memory_model = Target::MemoryModel::Unexpanded;
|
2018-04-01 00:58:16 +00:00
|
|
|
break;
|
2016-09-13 11:26:51 +00:00
|
|
|
case 0x1201:
|
2019-12-27 03:49:48 +00:00
|
|
|
memory_model = Target::MemoryModel::ThirtyTwoKB;
|
2016-09-13 11:26:51 +00:00
|
|
|
break;
|
|
|
|
case 0x0401:
|
2019-12-27 03:49:48 +00:00
|
|
|
memory_model = Target::MemoryModel::EightKB;
|
2016-09-13 11:26:51 +00:00
|
|
|
break;
|
2024-12-02 20:27:23 +00:00
|
|
|
|
|
|
|
case 0x1c01:
|
|
|
|
Log::Logger<Log::Source::CommodoreStaticAnalyser>().info().append("Unimplemented: C128");
|
|
|
|
break;
|
2016-09-13 11:26:51 +00:00
|
|
|
}
|
|
|
|
|
2019-12-27 03:49:48 +00:00
|
|
|
target->set_memory_model(memory_model);
|
|
|
|
|
2016-09-13 11:26:51 +00:00
|
|
|
// General approach: increase memory size conservatively such that the largest file found will fit.
|
2018-03-25 17:37:33 +00:00
|
|
|
// for(File &file : files) {
|
|
|
|
// std::size_t file_size = file.data.size();
|
2016-09-13 11:26:51 +00:00
|
|
|
// bool is_basic = file.is_basic();
|
2016-09-09 01:09:37 +00:00
|
|
|
|
2016-09-13 11:26:51 +00:00
|
|
|
/*if(is_basic)
|
2016-09-13 02:06:03 +00:00
|
|
|
{
|
2016-09-13 11:26:51 +00:00
|
|
|
// BASIC files may be relocated, so the only limit is size.
|
|
|
|
//
|
|
|
|
// An unexpanded machine has 3583 bytes free for BASIC;
|
|
|
|
// a 3kb expanded machine has 6655 bytes free.
|
|
|
|
if(file_size > 6655)
|
2018-01-25 03:35:54 +00:00
|
|
|
target->vic20.memory_model = Vic20MemoryModel::ThirtyTwoKB;
|
|
|
|
else if(target->vic20.memory_model == Vic20MemoryModel::Unexpanded && file_size > 3583)
|
|
|
|
target->vic20.memory_model = Vic20MemoryModel::EightKB;
|
2016-09-13 02:06:03 +00:00
|
|
|
}
|
2016-09-13 11:26:51 +00:00
|
|
|
else
|
|
|
|
{*/
|
|
|
|
// if(!file.type == File::NonRelocatableProgram)
|
|
|
|
// {
|
|
|
|
// Non-BASIC files may be relocatable but, if so, by what logic?
|
|
|
|
// Given that this is unknown, take starting address as literal
|
|
|
|
// and check against memory windows.
|
|
|
|
//
|
|
|
|
// (ignoring colour memory...)
|
|
|
|
// An unexpanded Vic has memory between 0x0000 and 0x0400; and between 0x1000 and 0x2000.
|
|
|
|
// A 3kb expanded Vic fills in the gap and has memory between 0x0000 and 0x2000.
|
|
|
|
// A 32kb expanded Vic has memory in the entire low 32kb.
|
2018-03-25 17:37:33 +00:00
|
|
|
// uint16_t starting_address = file.starting_address;
|
2016-09-09 01:09:37 +00:00
|
|
|
|
2016-09-13 11:26:51 +00:00
|
|
|
// If anything above the 8kb mark is touched, mark as a 32kb machine; otherwise if the
|
|
|
|
// region 0x0400 to 0x1000 is touched and this is an unexpanded machine, mark as 3kb.
|
2018-03-25 17:37:33 +00:00
|
|
|
// if(starting_address + file_size > 0x2000)
|
|
|
|
// target->memory_model = Target::MemoryModel::ThirtyTwoKB;
|
2024-11-30 02:08:35 +00:00
|
|
|
// else if(target->memory_model == Target::MemoryModel::Unexpanded &&
|
|
|
|
// !(starting_address >= 0x1000 || starting_address+file_size < 0x0400))
|
2018-03-25 17:37:33 +00:00
|
|
|
// target->memory_model = Target::MemoryModel::ThirtyTwoKB;
|
2016-09-13 11:26:51 +00:00
|
|
|
// }
|
2018-03-25 17:37:33 +00:00
|
|
|
// }
|
2016-09-06 10:39:40 +00:00
|
|
|
}
|
2016-09-07 11:39:47 +00:00
|
|
|
|
2018-04-06 21:42:24 +00:00
|
|
|
if(!target->media.empty()) {
|
2019-12-27 03:49:48 +00:00
|
|
|
// Inspect filename for configuration hints.
|
2018-04-06 21:42:24 +00:00
|
|
|
std::string lowercase_name = file_name;
|
|
|
|
std::transform(lowercase_name.begin(), lowercase_name.end(), lowercase_name.begin(), ::tolower);
|
2019-12-27 03:49:48 +00:00
|
|
|
|
|
|
|
// Hint 1: 'ntsc' anywhere in the name implies America.
|
2018-04-06 21:42:24 +00:00
|
|
|
if(lowercase_name.find("ntsc") != std::string::npos) {
|
|
|
|
target->region = Analyser::Static::Commodore::Target::Region::American;
|
|
|
|
}
|
|
|
|
|
2019-12-27 03:49:48 +00:00
|
|
|
// Potential additional hints: check for TheC64 tags.
|
|
|
|
auto final_underscore = lowercase_name.find_last_of('_');
|
|
|
|
if(final_underscore != std::string::npos) {
|
|
|
|
auto iterator = lowercase_name.begin() + ssize_t(final_underscore) + 1;
|
|
|
|
|
|
|
|
while(iterator != lowercase_name.end()) {
|
|
|
|
// Grab the next tag.
|
|
|
|
char next_tag[3] = {0, 0, 0};
|
|
|
|
next_tag[0] = *iterator++;
|
|
|
|
if(iterator == lowercase_name.end()) break;
|
|
|
|
next_tag[1] = *iterator++;
|
|
|
|
|
|
|
|
// Exit early if attempting to read another tag has run over the file extension.
|
|
|
|
if(next_tag[0] == '.' || next_tag[1] == '.') break;
|
|
|
|
|
|
|
|
// Check whether it's anything.
|
|
|
|
target->enabled_ram.bank0 |= !strcmp(next_tag, "b0");
|
|
|
|
target->enabled_ram.bank1 |= !strcmp(next_tag, "b1");
|
|
|
|
target->enabled_ram.bank2 |= !strcmp(next_tag, "b2");
|
|
|
|
target->enabled_ram.bank3 |= !strcmp(next_tag, "b3");
|
|
|
|
target->enabled_ram.bank5 |= !strcmp(next_tag, "b5");
|
|
|
|
if(!strcmp(next_tag, "tn")) { // i.e. NTSC.
|
|
|
|
target->region = Analyser::Static::Commodore::Target::Region::American;
|
|
|
|
}
|
|
|
|
if(!strcmp(next_tag, "tp")) { // i.e. PAL.
|
|
|
|
target->region = Analyser::Static::Commodore::Target::Region::European;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unhandled:
|
|
|
|
//
|
2023-05-12 18:14:45 +00:00
|
|
|
// M6: this is a C64 file.
|
|
|
|
// MV: this is a Vic-20 file.
|
2019-12-27 03:49:48 +00:00
|
|
|
// J1/J2: this C64 file should have the primary joystick in slot 1/2.
|
|
|
|
// RO: this disk image should be treated as read-only.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-08 21:37:39 +00:00
|
|
|
// Attach a 1540 if there are any disks here.
|
|
|
|
target->has_c1540 = !target->media.disks.empty();
|
|
|
|
|
2018-01-25 03:35:54 +00:00
|
|
|
destination.push_back(std::move(target));
|
2018-04-06 21:42:24 +00:00
|
|
|
}
|
2018-04-14 16:12:12 +00:00
|
|
|
|
|
|
|
return destination;
|
2016-09-08 02:17:19 +00:00
|
|
|
}
|