diff --git a/Boxer/DOS window/BXDOSWindowController.h b/Boxer/DOS window/BXDOSWindowController.h index 12c0c80d..0d7d485f 100644 --- a/Boxer/DOS window/BXDOSWindowController.h +++ b/Boxer/DOS window/BXDOSWindowController.h @@ -170,6 +170,10 @@ extern NSNotificationName const BXViewDidLiveResizeNotification; /// Toggle the emulator's active rendering filter. - (IBAction) toggleRenderingStyle: (id)sender; +/// Refresh and select presets from the bundled custom shader menu. +- (IBAction) refreshShaderPresetMenu: (id)sender; +- (IBAction) selectShaderPreset: (id)sender; + /// Increase the draw size of the fullscreen window. - (IBAction) incrementFullscreenSize: (id)sender; /// Decrease the draw size of the fullscreen window. diff --git a/Boxer/DOS window/BXDOSWindowController.m b/Boxer/DOS window/BXDOSWindowController.m index 8579ac0a..cc643877 100644 --- a/Boxer/DOS window/BXDOSWindowController.m +++ b/Boxer/DOS window/BXDOSWindowController.m @@ -166,6 +166,15 @@ - (void) windowDidLoad //Display the loading panel by default. [self switchToPanel: BXDOSWindowLoadingPanel animate: NO]; + + NSString *savedShaderPresetPath = [[NSUserDefaults standardUserDefaults] stringForKey:@"shaderPresetPath"]; + if (savedShaderPresetPath.length > 0) + { + if (![self.renderingView loadShaderPresetAtPath:savedShaderPresetPath]) + { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"shaderPresetPath"]; + } + } self.window.preservesContentDuringLiveResize = NO; self.window.acceptsMouseMovedEvents = YES; @@ -458,10 +467,46 @@ - (IBAction) toggleShaderParametersWindow: (id)sender - (IBAction) toggleRenderingStyle: (id )sender { BXRenderingStyle style = (BXRenderingStyle)sender.tag; + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"shaderPresetPath"]; + self.renderingView.renderingStyle = style; [[NSUserDefaults standardUserDefaults] setInteger: style forKey: @"renderingStyle"]; } +- (IBAction)refreshShaderPresetMenu:(id)sender +{ + // The menu is populated during validation immediately before it is displayed. +} + +- (IBAction)selectShaderPreset:(NSMenuItem *)sender +{ + NSString *presetPath = sender.representedObject; + if (![presetPath isKindOfClass:NSString.class]) + { + return; + } + + if ([self.renderingView loadShaderPresetAtPath:presetPath]) + { + [[NSUserDefaults standardUserDefaults] setObject:presetPath forKey:@"shaderPresetPath"]; + } +} + +- (void)populateShaderPresetMenu:(NSMenu *)menu +{ + [menu removeAllItems]; + for (NSString *presetPath in self.renderingView.availableShaderPresetPaths) + { + NSString *title = presetPath.lastPathComponent.stringByDeletingPathExtension; + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title + action:@selector(selectShaderPreset:) + keyEquivalent:@""]; + item.target = nil; + item.representedObject = presetPath; + [menu addItem:item]; + } +} + - (IBAction) toggleHerculesTintMode: (id )sender { BXHerculesTintMode tint = (BXHerculesTintMode)sender.tag; @@ -926,7 +971,8 @@ - (BOOL) validateMenuItem: (NSMenuItem *)theItem if (theAction == @selector(toggleRenderingStyle:)) { BXRenderingStyle renderingStyle = (BXRenderingStyle)theItem.tag; - if (renderingStyle == self.renderingStyle) + if (renderingStyle == self.renderingStyle && + [[NSUserDefaults standardUserDefaults] stringForKey:@"shaderPresetPath"].length == 0) { theItem.state = NSControlStateValueOn; } @@ -934,8 +980,22 @@ - (BOOL) validateMenuItem: (NSMenuItem *)theItem { theItem.state = NSControlStateValueOff; } - return YES; - } + return YES; + } + + if (theAction == @selector(refreshShaderPresetMenu:)) + { + [self populateShaderPresetMenu:theItem.submenu]; + return YES; + } + + if (theAction == @selector(selectShaderPreset:)) + { + NSString *presetPath = theItem.representedObject; + theItem.state = [presetPath isEqualToString:self.renderingView.selectedShaderPresetPath] + ? NSControlStateValueOn : NSControlStateValueOff; + return YES; + } if (theAction == @selector(toggleHerculesTintMode:)) { diff --git a/Boxer/Metal Rendering/BXMetalRenderingView.m b/Boxer/Metal Rendering/BXMetalRenderingView.m index 3f1b5c38..a929e0ee 100644 --- a/Boxer/Metal Rendering/BXMetalRenderingView.m +++ b/Boxer/Metal Rendering/BXMetalRenderingView.m @@ -46,6 +46,7 @@ @implementation BXMetalRenderingView { NSRect _viewportRect; NSRect _targetViewportRect; BXRenderingStyle _renderingStyle; + NSString *_selectedShaderPresetPath; } @synthesize currentFrame=_currentFrame; @@ -100,39 +101,94 @@ - (BOOL)supportsRenderingStyle:(BXRenderingStyle)style { return YES; } -- (void)setRenderingStyle:(BXRenderingStyle)renderingStyle { - if (renderingStyle == _renderingStyle) +- (NSArray *)availableShaderPresetPaths +{ + NSURL *shadersURL = [NSBundle.mainBundle.resourceURL URLByAppendingPathComponent:@"Shaders" isDirectory:YES]; + NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] + enumeratorAtURL:shadersURL + includingPropertiesForKeys:@[NSURLIsRegularFileKey] + options:(NSDirectoryEnumerationSkipsHiddenFiles | NSDirectoryEnumerationSkipsPackageDescendants) + errorHandler:nil]; + + if (enumerator == nil) { - return; + return @[]; } - + + NSMutableArray *presetPaths = [NSMutableArray array]; + for (NSURL *presetURL in enumerator) + { + NSNumber *isRegularFile = nil; + [presetURL getResourceValue:&isRegularFile forKey:NSURLIsRegularFileKey error:nil]; + if (!isRegularFile.boolValue || ![presetURL.pathExtension.lowercaseString isEqualToString:@"slangp"]) + { + continue; + } + + NSString *relativePath = [presetURL.path substringFromIndex:shadersURL.path.length]; + relativePath = [relativePath stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/"]]; + if (relativePath.length > 0) + { + [presetPaths addObject:relativePath]; + } + } + + [presetPaths sortUsingComparator:^NSComparisonResult(NSString *first, NSString *second) { + return [first.lastPathComponent localizedStandardCompare:second.lastPathComponent]; + }]; + return presetPaths; +} + +- (NSString *)selectedShaderPresetPath +{ + return _selectedShaderPresetPath; +} + +- (BOOL)loadShaderPresetAtPath:(NSString *)presetPath +{ + if (presetPath.length == 0 || ![self.availableShaderPresetPaths containsObject:presetPath]) + { + return NO; + } + + NSURL *shadersURL = [NSBundle.mainBundle.resourceURL URLByAppendingPathComponent:@"Shaders" isDirectory:YES]; + NSURL *presetURL = [shadersURL URLByAppendingPathComponent:presetPath]; + NSError *error = nil; + if (![_filterChain setShaderFromURL:presetURL error:&error]) + { + NSLog(@"Could not load shader preset at %@: %@", presetPath, error.localizedDescription); + return NO; + } + + _selectedShaderPresetPath = [presetPath copy]; + self.parameterGroups = _filterChain.shader.parameterGroups; + return YES; +} + +- (void)setRenderingStyle:(BXRenderingStyle)renderingStyle { [self willChangeValueForKey:@"renderingStyle"]; _renderingStyle = renderingStyle; switch (renderingStyle) { case BXRenderingStyleNormal: { - NSURL *path = [NSBundle.mainBundle URLForResource:@"Pixellate" withExtension:@"slangp" subdirectory:@"Shaders/Pixellate"]; - [_filterChain setShaderFromURL:path error:nil]; + [self loadShaderPresetAtPath:@"Pixellate/Pixellate.slangp"]; break; } case BXRenderingStyleCRT: { - NSURL *path = [NSBundle.mainBundle URLForResource:@"CRT Geom" withExtension:@"slangp" subdirectory:@"Shaders/CRT Geom"]; - [_filterChain setShaderFromURL:path error:nil]; + [self loadShaderPresetAtPath:@"CRT Geom/CRT Geom.slangp"]; break; } case BXRenderingStyleSmoothed: { - NSURL *path = [NSBundle.mainBundle URLForResource:@"Smooth" withExtension:@"slangp" subdirectory:@"Shaders/Smooth"]; - [_filterChain setShaderFromURL:path error:nil]; + [self loadShaderPresetAtPath:@"Smooth/Smooth.slangp"]; break; } } [self didChangeValueForKey:@"renderingStyle"]; - self.parameterGroups = _filterChain.shader.parameterGroups; } - (void)updateWithFrame:(BXVideoFrame *)frame { diff --git a/Boxer/Rendering/BXFrameRenderingView.h b/Boxer/Rendering/BXFrameRenderingView.h index c200a7c8..ce5af1df 100644 --- a/Boxer/Rendering/BXFrameRenderingView.h +++ b/Boxer/Rendering/BXFrameRenderingView.h @@ -36,6 +36,16 @@ typedef NS_ENUM(NSInteger, BXRenderingStyle) { /// Set/get the current rendering style of the view. @property (readwrite, nonatomic) BXRenderingStyle renderingStyle; +/// Relative paths of the shader presets available in the application bundle. +@property (readonly, nonatomic) NSArray *availableShaderPresetPaths; + +/// The relative path of the currently loaded shader preset, if known. +@property (readonly, nullable, nonatomic) NSString *selectedShaderPresetPath; + +/// Loads a shader preset discovered in availableShaderPresetPaths. +/// Returns NO without changing the current shader if the preset cannot be found or loaded. +- (BOOL)loadShaderPresetAtPath:(NSString *)presetPath; + /// Whether this rendering view can render in the specified style. - (BOOL) supportsRenderingStyle: (BXRenderingStyle)style; diff --git a/Resources/Base.lproj/MainMenu.xib b/Resources/Base.lproj/MainMenu.xib index 4fa1ba34..fbc8cb05 100644 --- a/Resources/Base.lproj/MainMenu.xib +++ b/Resources/Base.lproj/MainMenu.xib @@ -509,6 +509,14 @@ + + + + + + + + diff --git a/Resources/mul.lproj/MainMenu.xcstrings b/Resources/mul.lproj/MainMenu.xcstrings index 13d2fbdf..c4f60ed5 100644 --- a/Resources/mul.lproj/MainMenu.xcstrings +++ b/Resources/mul.lproj/MainMenu.xcstrings @@ -5011,6 +5011,30 @@ } } }, + "custom-shaders-item.title" : { + "comment" : "Class = \"NSMenuItem\"; title = \"Custom Shaders\"; ObjectID = \"custom-shaders-item\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Custom Shaders" + } + } + } + }, + "custom-shaders-menu.title" : { + "comment" : "Class = \"NSMenu\"; title = \"Custom Shaders\"; ObjectID = \"custom-shaders-menu\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Custom Shaders" + } + } + } + }, "Wks-tC-vJz.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Shader Properties…\"; ObjectID = \"Wks-tC-vJz\";", "extractionState" : "extracted_with_value",