mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-25 09:27:01 +00:00
Compare commits
25 Commits
2020-11-13
...
2020-12-09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8560b38ffa | ||
|
|
94eb17db0c | ||
|
|
9577c8e27f | ||
|
|
32ccce3040 | ||
|
|
ab3fcb3ea0 | ||
|
|
9610672615 | ||
|
|
5ee9630624 | ||
|
|
1b3836eb1c | ||
|
|
1302a046e9 | ||
|
|
33dec3c220 | ||
|
|
7c29c3a944 | ||
|
|
c9ca1fc7a0 | ||
|
|
a965c8de9f | ||
|
|
0b4b271e3d | ||
|
|
5fc6dd1a4d | ||
|
|
79ef026b93 | ||
|
|
a4ab5b0b49 | ||
|
|
3207183f05 | ||
|
|
e803f993b7 | ||
|
|
5dbc87caf0 | ||
|
|
4862ccc947 | ||
|
|
e1ecf66485 | ||
|
|
2c71ba0744 | ||
|
|
a7aeb779e9 | ||
|
|
e72cfbf447 |
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ['https://www.amazon.com/hz/wishlist/ls/8WPVFLQQDPTA']
|
||||
# Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -804,38 +804,62 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
m6502_.run_for(cycles);
|
||||
}
|
||||
|
||||
void reset_all_keys(Inputs::Keyboard *) final {
|
||||
void reset_all_keys() final {
|
||||
open_apple_is_pressed_ = closed_apple_is_pressed_ = key_is_down_ = false;
|
||||
}
|
||||
|
||||
bool prefers_logical_input() final {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool set_key_pressed(Key key, char value, bool is_pressed) final {
|
||||
// If no ASCII value is supplied, look for a few special cases.
|
||||
switch(key) {
|
||||
default: break;
|
||||
case Key::F12:
|
||||
m6502_.set_reset_line(is_pressed);
|
||||
return true;
|
||||
case Key::Left: value = 0x08; break;
|
||||
case Key::Right: value = 0x15; break;
|
||||
case Key::Down: value = 0x0a; break;
|
||||
case Key::Up: value = 0x0b; break;
|
||||
case Key::Backspace: value = 0x7f; break;
|
||||
case Key::Enter: value = 0x0d; break;
|
||||
case Key::Tab: value = '\t'; break;
|
||||
case Key::Escape: value = 0x1b; break;
|
||||
|
||||
case Key::LeftOption:
|
||||
case Key::RightMeta:
|
||||
open_apple_is_pressed_ = is_pressed;
|
||||
return true;
|
||||
|
||||
case Key::RightOption:
|
||||
case Key::LeftMeta:
|
||||
closed_apple_is_pressed_ = is_pressed;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If no ASCII value is supplied, look for a few special cases.
|
||||
if(!value) {
|
||||
switch(key) {
|
||||
case Key::Left: value = 0x08; break;
|
||||
case Key::Right: value = 0x15; break;
|
||||
case Key::Down: value = 0x0a; break;
|
||||
case Key::Up: value = 0x0b; break;
|
||||
case Key::Backspace: value = 0x7f; break;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
case Key::F1: case Key::F2: case Key::F3: case Key::F4:
|
||||
case Key::F5: case Key::F6: case Key::F7: case Key::F8:
|
||||
case Key::F9: case Key::F10: case Key::F11: case Key::F12:
|
||||
case Key::PrintScreen:
|
||||
case Key::ScrollLock:
|
||||
case Key::Pause:
|
||||
case Key::Insert:
|
||||
case Key::Home:
|
||||
case Key::PageUp:
|
||||
case Key::PageDown:
|
||||
case Key::End:
|
||||
// Accept a bunch non-symbolic other keys, as
|
||||
// reset, in the hope that the user can find
|
||||
// at least one usable key.
|
||||
m6502_.set_reset_line(is_pressed);
|
||||
return true;
|
||||
|
||||
// Prior to the IIe, the keyboard could produce uppercase only.
|
||||
if(!is_iie()) value = char(toupper(value));
|
||||
default:
|
||||
if(!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prior to the IIe, the keyboard could produce uppercase only.
|
||||
if(!is_iie()) value = char(toupper(value));
|
||||
break;
|
||||
}
|
||||
|
||||
if(is_pressed) {
|
||||
keyboard_input_ = uint8_t(value | 0x80);
|
||||
|
||||
@@ -32,6 +32,11 @@ struct KeyActions {
|
||||
Instructs that all keys should now be treated as released.
|
||||
*/
|
||||
virtual void clear_all_keys() {}
|
||||
|
||||
/*!
|
||||
Indicates whether a machine most naturally accepts logical rather than physical input.
|
||||
*/
|
||||
virtual bool prefers_logical_input() { return false; }
|
||||
};
|
||||
|
||||
/*!
|
||||
|
||||
@@ -5175,7 +5175,7 @@
|
||||
"$(USER_LIBRARY_DIR)/Frameworks",
|
||||
);
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.13;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -5198,7 +5198,8 @@
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_OPTIMIZATION_LEVEL = 2;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = NDEBUG;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.13;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
@@ -5252,7 +5253,7 @@
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_PARAMETER = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.13;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -5305,7 +5306,7 @@
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_PARAMETER = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.13;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
@@ -5327,7 +5328,6 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Clock Signal/Clock Signal.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = DV3346VVUN;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -5373,7 +5373,6 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Clock Signal/Clock Signal.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = DV3346VVUN;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -5395,6 +5394,7 @@
|
||||
INFOPLIST_FILE = "Clock Signal/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
MTL_TREAT_WARNINGS_AS_ERRORS = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CPLUSPLUSFLAGS = (
|
||||
"$(OTHER_CFLAGS)",
|
||||
"-Wreorder",
|
||||
@@ -5416,7 +5416,6 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Clock Signal/Clock Signal.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
INFOPLIST_FILE = "Clock SignalTests/Info.plist";
|
||||
@@ -5440,7 +5439,6 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Clock Signal/Clock Signal.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = 2;
|
||||
@@ -5458,7 +5456,6 @@
|
||||
4BB73ECD1B587A5100552FC2 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = CP2SKEB3XT;
|
||||
INFOPLIST_FILE = "Clock SignalUITests/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
|
||||
@@ -5473,7 +5470,6 @@
|
||||
4BB73ECE1B587A5100552FC2 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = CP2SKEB3XT;
|
||||
INFOPLIST_FILE = "Clock SignalUITests/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
disableMainThreadChecker = "YES"
|
||||
@@ -58,8 +58,16 @@
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = ""/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Macintosh/MusicWorks 0.42.image""
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "/Users/thomasharte/Library/Mobile\ Documents/com\~apple\~CloudDocs/Desktop/Soft/Apple\ II/Keplermatik.dsk"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "/Users/thomasharte/Library/Mobile\ Documents/com\~apple\~CloudDocs/Desktop/Soft/Apple\ II/WOZs/Prince\ of\ Persia\ side\ A.woz"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--volume=0.001"
|
||||
isEnabled = "NO">
|
||||
|
||||
@@ -88,14 +88,30 @@ class MachineDocument:
|
||||
}
|
||||
|
||||
override func close() {
|
||||
// Close any dangling sheets.
|
||||
//
|
||||
// Be warned: in 11.0 at least, if there are any panels then posting the endSheet request
|
||||
// will defer the close(), and close() will be called again at the end of that animation.
|
||||
//
|
||||
// So: MAKE SURE IT'S SAFE TO ENTER THIS FUNCTION TWICE. Hence the non-assumption here about
|
||||
// any windows still existing.
|
||||
if self.windowControllers.count > 0, let window = self.windowControllers[0].window {
|
||||
for sheet in window.sheets {
|
||||
window.endSheet(sheet)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the machine, if any.
|
||||
machine?.stop()
|
||||
|
||||
// Dismiss panels.
|
||||
activityPanel?.setIsVisible(false)
|
||||
activityPanel = nil
|
||||
|
||||
optionsPanel?.setIsVisible(false)
|
||||
optionsPanel = nil
|
||||
|
||||
// End the update cycle.
|
||||
actionLock.lock()
|
||||
drawLock.lock()
|
||||
machine = nil
|
||||
@@ -103,6 +119,7 @@ class MachineDocument:
|
||||
actionLock.unlock()
|
||||
drawLock.unlock()
|
||||
|
||||
// Let the document controller do its thing.
|
||||
super.close()
|
||||
}
|
||||
|
||||
@@ -144,35 +161,37 @@ class MachineDocument:
|
||||
private var interactionMode: InteractionMode = .notStarted
|
||||
|
||||
// Attempting to show a sheet before the window is visible (such as when the NIB is loaded) results in
|
||||
// a sheet mysteriously floating on its own. For now, use windowDidUpdate as a proxy to know that the window
|
||||
// is visible, though it's a little premature.
|
||||
// a sheet mysteriously floating on its own. For now, use windowDidUpdate as a proxy to check whether
|
||||
// the window is visible.
|
||||
func windowDidUpdate(_ notification: Notification) {
|
||||
// Grab the regular window title, if it's not already stored.
|
||||
if self.unadornedWindowTitle.count == 0 {
|
||||
self.unadornedWindowTitle = self.windowControllers[0].window!.title
|
||||
}
|
||||
|
||||
// If an interaction mode is not yet in effect, pick the proper one and display the relevant thing.
|
||||
if self.interactionMode == .notStarted {
|
||||
// If a full machine exists, just continue showing it.
|
||||
if self.machine != nil {
|
||||
self.interactionMode = .showingMachine
|
||||
setupMachineOutput()
|
||||
return
|
||||
if self.windowControllers.count > 0, let window = self.windowControllers[0].window, window.isVisible {
|
||||
// Grab the regular window title, if it's not already stored.
|
||||
if self.unadornedWindowTitle.count == 0 {
|
||||
self.unadornedWindowTitle = window.title
|
||||
}
|
||||
|
||||
// If a machine has been picked but is not showing, there must be ROMs missing.
|
||||
if self.machineDescription != nil {
|
||||
self.interactionMode = .showingROMRequester
|
||||
requestRoms()
|
||||
return
|
||||
}
|
||||
// If an interaction mode is not yet in effect, pick the proper one and display the relevant thing.
|
||||
if self.interactionMode == .notStarted {
|
||||
// If a full machine exists, just continue showing it.
|
||||
if self.machine != nil {
|
||||
self.interactionMode = .showingMachine
|
||||
setupMachineOutput()
|
||||
return
|
||||
}
|
||||
|
||||
// If a machine hasn't even been picked yet, show the machine picker.
|
||||
self.interactionMode = .showingMachinePicker
|
||||
Bundle.main.loadNibNamed("MachinePicker", owner: self, topLevelObjects: nil)
|
||||
self.machinePicker?.establishStoredOptions()
|
||||
self.windowControllers[0].window?.beginSheet(self.machinePickerPanel!, completionHandler: nil)
|
||||
// If a machine has been picked but is not showing, there must be ROMs missing.
|
||||
if self.machineDescription != nil {
|
||||
self.interactionMode = .showingROMRequester
|
||||
requestRoms()
|
||||
return
|
||||
}
|
||||
|
||||
// If a machine hasn't even been picked yet, show the machine picker.
|
||||
self.interactionMode = .showingMachinePicker
|
||||
Bundle.main.loadNibNamed("MachinePicker", owner: self, topLevelObjects: nil)
|
||||
self.machinePicker?.establishStoredOptions()
|
||||
window.beginSheet(self.machinePickerPanel!, completionHandler: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,11 @@ class MachinePicker: NSObject {
|
||||
|
||||
// Machine type
|
||||
if let machineIdentifier = standardUserDefaults.string(forKey: "new.machine") {
|
||||
machineSelector?.selectTabViewItem(withIdentifier: machineIdentifier as Any)
|
||||
// If I've changed my mind about visible tabs between versions, there may not be one that corresponds
|
||||
// to the stored identifier. Make sure not to raise an NSRangeException in that scenario.
|
||||
if let index = machineSelector?.indexOfTabViewItem(withIdentifier: machineIdentifier as Any), index != NSNotFound {
|
||||
machineSelector?.selectTabViewItem(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
// Apple II settings
|
||||
|
||||
@@ -1020,7 +1020,7 @@ bool MainWindow::processEvent(QKeyEvent *event) {
|
||||
const auto keyboardMachine = machine->keyboard_machine();
|
||||
if(!keyboardMachine) return true;
|
||||
|
||||
auto keyboard = keyboardMachine->get_keyboard();
|
||||
auto &keyboard = keyboardMachine->get_keyboard();
|
||||
keyboard.set_key_pressed(*key, event->text().size() ? event->text()[0].toLatin1() : '\0', isPressed);
|
||||
if(keyboard.is_exclusive() || keyboard.observed_keys().find(*key) != keyboard.observed_keys().end()) {
|
||||
return false;
|
||||
|
||||
@@ -783,8 +783,10 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether a 'logical' keyboard has been requested.
|
||||
const bool logical_keyboard = arguments.selections.find("logical-keyboard") != arguments.selections.end();
|
||||
// Check whether a 'logical' keyboard has been requested, or the machine would prefer one anyway.
|
||||
const bool logical_keyboard =
|
||||
(arguments.selections.find("logical-keyboard") != arguments.selections.end()) ||
|
||||
(machine->keyboard_machine() && machine->keyboard_machine()->prefers_logical_input());
|
||||
if(logical_keyboard) {
|
||||
SDL_StartTextInput();
|
||||
}
|
||||
@@ -1162,13 +1164,8 @@ int main(int argc, char *argv[]) {
|
||||
} else {
|
||||
// This is a slightly terrible way of obtaining a symbol for the key, e.g. for letters it will always return
|
||||
// the capital letter version, at least empirically. But it'll have to do for now.
|
||||
//
|
||||
// TODO: ideally have a keyboard machine declare whether it wants either key events or text input? But that
|
||||
// doesn't match machines like the IIe that, to some extent, expose both. So then eliding as attempted above,
|
||||
// and keeping ephemeral track of which symbols have been tied to which keys for the benefit of future key up
|
||||
// events is probably the way forward?
|
||||
const char *key_name = SDL_GetKeyName(keypress.keycode);
|
||||
if(keyboard_machine->get_keyboard().set_key_pressed(key, key_name[0], keypress.is_down)) {
|
||||
if(keyboard_machine->get_keyboard().set_key_pressed(key, (strlen(key_name) == 1) ? key_name[0] : 0, keypress.is_down)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,8 +194,9 @@ std::vector<std::string> ScanTarget::bindings(ShaderType type) const {
|
||||
std::string ScanTarget::sampling_function() const {
|
||||
std::string fragment_shader;
|
||||
const auto modals = BufferingScanTarget::modals();
|
||||
const bool is_svideo = modals.display_type == DisplayType::SVideo;
|
||||
|
||||
if(modals.display_type == DisplayType::SVideo) {
|
||||
if(is_svideo) {
|
||||
fragment_shader +=
|
||||
"vec2 svideo_sample(vec2 coordinate, float angle) {";
|
||||
} else {
|
||||
@@ -203,7 +204,6 @@ std::string ScanTarget::sampling_function() const {
|
||||
"float composite_sample(vec2 coordinate, float angle) {";
|
||||
}
|
||||
|
||||
const bool is_svideo = modals.display_type == DisplayType::SVideo;
|
||||
switch(modals.input_data_type) {
|
||||
case InputDataType::Luminance1:
|
||||
case InputDataType::Luminance8:
|
||||
@@ -341,7 +341,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
|
||||
vertex_shader +=
|
||||
"compositeAngle = (mix(startCompositeAngle, endCompositeAngle, lateral) / 32.0) * 3.141592654;"
|
||||
"compositeAmplitude = lineCompositeAmplitude / 255.0;"
|
||||
"oneOverCompositeAmplitude = mix(0.0, 255.0 / lineCompositeAmplitude, step(0.01, lineCompositeAmplitude));";
|
||||
"oneOverCompositeAmplitude = mix(0.0, 255.0 / lineCompositeAmplitude, step(0.95, lineCompositeAmplitude));";
|
||||
}
|
||||
|
||||
vertex_shader +=
|
||||
@@ -379,40 +379,42 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
|
||||
|
||||
switch(modals.display_type) {
|
||||
case DisplayType::CompositeColour:
|
||||
fragment_shader +=
|
||||
"vec4 angles = compositeAngle + compositeAngleOffsets;"
|
||||
fragment_shader += R"x(
|
||||
vec4 angles = compositeAngle + compositeAngleOffsets;
|
||||
|
||||
// Sample four times over, at proper angle offsets.
|
||||
"vec4 samples = vec4("
|
||||
"composite_sample(textureCoordinates[0], angles.x),"
|
||||
"composite_sample(textureCoordinates[1], angles.y),"
|
||||
"composite_sample(textureCoordinates[2], angles.z),"
|
||||
"composite_sample(textureCoordinates[3], angles.w)"
|
||||
");"
|
||||
vec4 samples = vec4(
|
||||
composite_sample(textureCoordinates[0], angles.x),
|
||||
composite_sample(textureCoordinates[1], angles.y),
|
||||
composite_sample(textureCoordinates[2], angles.z),
|
||||
composite_sample(textureCoordinates[3], angles.w)
|
||||
);
|
||||
|
||||
// Compute a luminance for use if there's no colour information, now, before
|
||||
// modifying samples.
|
||||
"float mono_luminance = dot(samples, vec4(0.15, 0.35, 0.35, 0.15));"
|
||||
// The outer structure of the OpenGL scan target means in practice that
|
||||
// compositeAmplitude will be the same value across a piece of
|
||||
// geometry. I am therefore optimistic that this conditional will not
|
||||
// cause a divergence in fragment execution.
|
||||
if(compositeAmplitude < 0.01) {
|
||||
// Compute only a luminance for use if there's no colour information.
|
||||
fragColour3 = vec3(dot(samples, vec4(0.15, 0.35, 0.35, 0.15)));
|
||||
} else {
|
||||
// Take the average to calculate luminance, then subtract that from all four samples to
|
||||
// give chrominance.
|
||||
float luminance = dot(samples, vec4(0.25));
|
||||
|
||||
// Take the average to calculate luminance, then subtract that from all four samples to
|
||||
// give chrominance.
|
||||
"float luminance = dot(samples, vec4(0.25));"
|
||||
// Split and average chrominance.
|
||||
vec2 chrominances[4] = vec2[4](
|
||||
textureLod(qamTextureName, qamTextureCoordinates[0], 0).gb,
|
||||
textureLod(qamTextureName, qamTextureCoordinates[1], 0).gb,
|
||||
textureLod(qamTextureName, qamTextureCoordinates[2], 0).gb,
|
||||
textureLod(qamTextureName, qamTextureCoordinates[3], 0).gb
|
||||
);
|
||||
vec2 channels = (chrominances[0] + chrominances[1] + chrominances[2] + chrominances[3])*0.5 - vec2(1.0);
|
||||
|
||||
// Split and average chrominance.
|
||||
"vec2 chrominances[4] = vec2[4]("
|
||||
"textureLod(qamTextureName, qamTextureCoordinates[0], 0).gb,"
|
||||
"textureLod(qamTextureName, qamTextureCoordinates[1], 0).gb,"
|
||||
"textureLod(qamTextureName, qamTextureCoordinates[2], 0).gb,"
|
||||
"textureLod(qamTextureName, qamTextureCoordinates[3], 0).gb"
|
||||
");"
|
||||
"vec2 channels = (chrominances[0] + chrominances[1] + chrominances[2] + chrominances[3])*0.5 - vec2(1.0);"
|
||||
|
||||
// Apply a colour space conversion to get RGB.
|
||||
"fragColour3 = mix("
|
||||
"lumaChromaToRGB * vec3(luminance / (1.0 - compositeAmplitude), channels),"
|
||||
"vec3(mono_luminance),"
|
||||
"step(oneOverCompositeAmplitude, 0.01)"
|
||||
");";
|
||||
// Apply a colour space conversion to get RGB.
|
||||
fragColour3 = lumaChromaToRGB * vec3(luminance / (1.0 - compositeAmplitude), channels);
|
||||
}
|
||||
)x";
|
||||
break;
|
||||
|
||||
case DisplayType::CompositeMonochrome:
|
||||
@@ -622,7 +624,7 @@ std::unique_ptr<Shader> ScanTarget::qam_separation_shader() const {
|
||||
|
||||
"compositeAngle = compositeAngle * 2.0 * 3.141592654;"
|
||||
"compositeAmplitude = lineCompositeAmplitude / 255.0;"
|
||||
"oneOverCompositeAmplitude = mix(0.0, 255.0 / lineCompositeAmplitude, step(0.01, lineCompositeAmplitude));";
|
||||
"oneOverCompositeAmplitude = mix(0.0, 255.0 / lineCompositeAmplitude, step(0.95, lineCompositeAmplitude));";
|
||||
|
||||
if(is_svideo) {
|
||||
vertex_shader +=
|
||||
|
||||
31
README.md
31
README.md
@@ -2,15 +2,12 @@
|
||||
# Clock Signal
|
||||
Clock Signal ('CLK') is an emulator for tourists that seeks to be invisible. Users directly launch classic software with no emulator or per-emulated-machine learning curve.
|
||||
|
||||
macOS and source releases are [hosted on GitHub](https://github.com/TomHarte/CLK/releases). For desktop Linux it is also available as a [Snap](https://snapcraft.io/clock-signal).
|
||||
macOS and source releases are [hosted on GitHub](https://github.com/TomHarte/CLK/releases). For desktop Linux it is also available as a [Snap](https://snapcraft.io/clock-signal). On the Mac it is a native Cocoa and Metal application; under Linux, BSD and other UNIXes and UNIX-alikes it uses OpenGL and can be built either with Qt or with SDL.
|
||||
|
||||
On the Mac it is a native Cocoa and Metal application; under Linux, BSD and other UNIXes and UNIX-alikes it uses OpenGL and can be built either with Qt or with SDL.
|
||||
|
||||
So its aims are:
|
||||
This emulator seeks to offer:
|
||||
* single-click load of any piece of source media for any supported platform;
|
||||
* with a heavy signal processing tilt for accurate reproduction of original outputs;
|
||||
* that aims for the lowest possible latency; and
|
||||
* 100% accurate emulations, naturally.
|
||||
* while minimising latency.
|
||||
|
||||
It currently contains emulations of the:
|
||||
* Acorn Electron;
|
||||
@@ -28,7 +25,7 @@ It currently contains emulations of the:
|
||||
|
||||
## Single-click Loading
|
||||
|
||||
Through the combination of static analysis and runtime analysis, CLK seeks to be able automatically to select and configure the appropriate machine to run any provided disk, tape or ROM; to issue any commands necessary to run the software contained on the disk, tape or ROM; and to provide accelerated loading where feasible.
|
||||
Through static and runtime analysis CLK seeks automatically to select and configure the appropriate machine to run any provided disk, tape or ROM; to issue any commands necessary to run the software contained on the disk, tape or ROM; and to provide accelerated loading where feasible.
|
||||
|
||||
The full process of loading a title — even if you've never used the emulated machine before — is therefore:
|
||||
|
||||
@@ -37,12 +34,10 @@ The full process of loading a title — even if you've never used the emulated m
|
||||
|
||||
## Signal Processing
|
||||
|
||||
Consider an ordinary, unmodified Commodore Vic-20. Its only video output is composite. Therefore the emulated machine's only video output is composite. In order to display the video output, your GPU then decodes composite video. Therefore all composite video artefacts are present and exactly correct, not because of a post hoc filter combining all the subjective effects that this author associates with composite video but because the real signal is really being processed.
|
||||
Consider an ordinary, unmodified Commodore Vic-20. Its only video output is composite. Therefore the emulated machine's only video output is composite. In order to display the video output, your GPU must decode composite video. Therefore composite video artefacts are present and correct not because of a post hoc filter but because the real signal is really being processed.
|
||||
|
||||
Similar effort is put into audio generation. If the real machine normally generates audio at 192Khz then the emulator generates a 192Khz source signal and filters it down to whatever the host machine can output.
|
||||
|
||||
If your machine has a 4k monitor and a 96Khz audio output? Then you'll get a 4k rendering of a composite display and, assuming the emulated machine produces source audio at or above 96Khz, 96,000 individual distinct audio samples a second. Interlaced video also works and looks much as it always did on those machines that produce it.
|
||||
|
||||
### Samples
|
||||
|
||||
| 1:1 Pixel Copying | Composite Decoded |
|
||||
@@ -61,15 +56,15 @@ If your machine has a 4k monitor and a 96Khz audio output? Then you'll get a 4k
|
||||
|
||||
## Low Latency
|
||||
|
||||
The display produced is an emulated CRT, with phosphor decay. Therefore if you have a 140Hz monitor it can produce 140 distinct frames per second. Latency is dictated by the output hardware, not the emulated machine.
|
||||
The display produced is an emulated CRT, with phosphor decay. Therefore if you have a 140Hz 4k monitor it can produce 140 distinct frames per second at 4k resolution. Latency is dictated by the host hardware, not the emulated machine or emulator.
|
||||
|
||||
The machine update mechanism is influenced separately by both screen refresh and audio stream processing; audio latency is therefore generally restrained to 5–10ms regardless of your screen's refresh rate.
|
||||
|
||||
A corollary of emulating the continuous nature CRT, not merely performing end-of-frame transcriptions, is that the most common motion aliasing effects of displaying 50Hz video on a 60Hz display are minimised; you don't have to own niche equipment to benefit.
|
||||
Audio latency is disjoint from frame rate and is generlaly restrained to 5–10ms.
|
||||
|
||||
## Accurate Emulation
|
||||
|
||||
Cycle-accurate emulation for the supported target machines is fairly trite; this emulator seeks to follow that precedent. All emulation logic is written in C++ for explicit control over costs but, where a conflict arises, the presumption is towards clarity and simplicity of code. This emulator is willing to spend the processing resources available on modern hardware.
|
||||
Accuracy affects usability; the more accurate an emulator, the more likely that a user can run every piece of software they're interested in without further intervention.
|
||||
|
||||
This emulator attempts cycle-accurate emulation of all supported machines. In some cases it succeeds.
|
||||
|
||||
## Additional Screenshots
|
||||
| | |
|
||||
@@ -83,3 +78,9 @@ Cycle-accurate emulation for the supported target machines is fairly trite; this
|
||||
|
||||

|
||||

|
||||
|
||||
## Sponsorship
|
||||
|
||||
I've been asked several times whether it is possible to sponsor this project; I think that's a poor fit for this emulator's highly-malleable scope, and it makes me uncomfortable because as the author I primarily see only its defects.
|
||||
|
||||
An Amazon US wishlist is now attached in the hope of avoiding the question in future. A lot of it is old books now available only secondhand — I like to read about potential future additions well in advance of starting on them. Per the optimism of some book sellers, please don't purchase anything that is currnetly listed only at an absurd price.
|
||||
|
||||
Reference in New Issue
Block a user