-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Metal implementation (using CIFilter) + Simplified Chinese translation #20
Conversation
- Bumped macOS target to 10.11 for MTKView - Bumped version to 2.1, build to 125 - macOS only resizing delegate
- Bump min deployment target to 10.13 to support Metal CIColorKernel init - Add fcikernel flags to Build Settings
…oats - Per correspondence with the authors, linearizing sRGB is not required despite the derivation. The publication also rendered its figures without. - Did not move HCIRN to use half-precision floats because I'm not familiar with the algo.
A FilterStore - Loads CIFilter from a FilterVendor. The app will have as many instances of a light-weight FilterVendor as there are filters known to initialize. - Holds a reference and instructs its application to a CIImage reference - Changes between vision simulations upon instruction
Swaps between HCIRN and Machado methodologies. Setup as a singelton defaulting to HCIRN. Forwards any user intents to the current Store.
- Move some objc constants into Swift temporarily into a consolidated Model folder
…mplementation of CGWindowListCreateImage - Still limited in performance by CGWindowListCreateImage at higher res. I should focus more on this than the sideshow effort below: - User setting to reduce performance simply skips some calls, as before - Uses CVDisplayLinkCreateWithActiveCGDisplays and its callback as a hardware refresh-synced timer, albiet generalized to all displays not just the current one. More precision would come from tracking the display showing the MTKView, possibly observing preferredFramesPerSecond as you pointed out.
- Manages Metal context for CIFilters and drives an MTKView - Relocated ScreenCapture into Metal folder as it's a potentially shared protocol with device-specific implementations
0.5 opacity would cause a problem (once not using OpenGL)
… Temporarily bump to 10.15 - Swapped for Filter View in Storyboard - Removed FilteredView - MTKView.preferredMetalDevice available w/ 10.15
…nslation - Handle intents in WindowController alongside VisionType - Indented several existing menu items and window menu items for consistency
- WindowController file diff shows up with false positive changes maybe from control I
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So that's a big PR, making it a bit difficult to review. But this new code structure makes several improvements so I think it's worth merging, and the new filters address issue #18.
This is a first round of comments. I should have more later.
General notes:
- It's a bit unfortunate that tabs were changed to spaces in many places; it creates "false" changes in the diff. Opened Project-level indentation settings #22.
- Also, for iOS the way forward will be to translate the remaining Objective-C code to Swift. Not in scope for this PR though.
}; | ||
1EF253061C27498600DB3B18 = { | ||
CreatedOnToolsVersion = 7.2; | ||
DevelopmentTeam = R5LCPCXJPZ; | ||
DevelopmentTeam = FVC2SFVFYG; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing development team settings is going to be a problem if I keep getting pull requests from you or others. Not something to address in this pull request, but I created issue #21.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Forgot to remove that before submitting. Apologies.
|
||
// Grab frame on main thread | ||
let initialFrame = view.frame | ||
filteredView.frame = initialFrame |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the current version, the filtered view's frame is inset a bit from the window edges (except on the top). This has two effects:
- the border is larger and more visible, which helps make the window visible given that discounting the shadow effect it is otherwise transparent
- the inset means there is no region near the edges where the window resize arrow cursor is showing up but you can't grab the window because it is ignoring mouse events
Insetting by 3 points on the sides and bottom edges should keep things working as they currently are.
|
||
func removeMonitorsFor(window: NSWindow) { | ||
let center = NotificationCenter.default | ||
center.removeObserver(self, name: NSWindow.didChangeOcclusionStateNotification, object: window) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this an oversight? You are removing the observer only for one notification here while setupMonitorForInteraction
above sets four observers.
return Self.shared | ||
} | ||
|
||
private init(vision: NSInteger, simulation: NSInteger) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit surprised by the usage of NSInteger
here and elsewhere.
- It's not needed in Swift: it's always equivalent to
Int
. In fact, all Apple APIs exposeNSInteger
asInt
in Swift. - Shouldn't these be cases in an
enum
so we have a nice list of constants to work with? Or is there a reason why not to use anenum
that I am overlooking?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed and good to know. I had delayed making a proper Swift enum in thinking I'd get the objc sorted so the model wouldn't be duplicated. But that's unnecessary. Will fix up.
Metal/Stores/HCIRNFilterStore.swift
Outdated
self.visionSimulation = vision | ||
DispatchQueue.global().async { [weak self] in | ||
self?.visionFilter = self?.loadFilter(for: vision) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this thread-safe? You are assigning to visionFilter
from a global queue here while the same visionFilter
variable is accessed by applyFilter
that gets called from the queue in CGWindowListScreenCapturer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. CIFilters are not thread safe. Yet, there are no user-adjusted dynamic values, so I figured it shouldn't rear its head? But, yeah, it's absolutely incorrect. I'll change how these filters are handled to make it clean and address another concern re: letting windows have different active filters.
Metal/MetalRenderer.swift
Outdated
|
||
/// Prepare the frame received by applying available filter(s). Call MTKView.draw(in:) to execute. | ||
func render(_ image: CIImage) { | ||
self.image = FilterStoreManager.shared.current.applyFilter(to: image) ?? image |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this thread-safe? The current
property here is accessed from the from the queue in CGWindowListScreenCapturer
, but the main thread can also change the current filter. Looks like a race condition to me.
Calling applyFilter
here appears to have the same problem. (See other comment.)
More thoughts about the FilterStore system of classes... As explained above it appears to have a thread safety issue. I think it'd be better if all the stateful parts where stored within the same class that would encapsulate all the read and write operations on that state. That would make thread safety issues easier to deal with. Another issue: the chosen filter is now stored as a global state. If the user creates multiple filter windows it is no longer possible to select a different filter for each one. |
Awesome lessons here for my first PR. 👍🏻 Thanks. I'll change up that system. Never thought of windows w/ separate filters. Neat use case. Thanks for the careful review. |
Fix MetalRenderer typo
Called by hosting ViewController in viewDidDisappear().
Write a DTO wrapper for persistence (window restoration or UD)
a2e2529
to
0da090c
Compare
@michelf Michel, I've come back round to this. The latest commits should address the to-dos identified earlier. Re: window bounds, the mouse monitor inset rect had a math error. I've fixed that and given it a few pixels more than prior. |
Thank you for those changes. I should be able to review this next weekend. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see there's now a FilterStoreManager
protocol with a single QueueOwningFilterStoreManager
implementation. I'm not sure what's the point of that. I'd encourage you to not build more structure than what's actually needed. This is particularly true here since we're not going to need many kinds of FilterStoreManager
in the app. In general, more structure means more code to maintain and more components to update when making changes later, so it's better to stay lean when you can.
I think the above comment also applies to the two FilterStore
s too. I think it'd be simpler to follow what the code is doing if they were both merged within the FilterStoreManager
with a single function having two-level switch in the style:
switch sim {
case .wicklineHCIRN:
switch vision {
case .normal: return nil
case .deutan: return CIFilter(name: vendor.DeutanFilterName)
...
}
case .machadoEtAl:
switch vision {
case .normal: return nil
...
}
}
To summarize, that structure you built for choosing the CIFilter
is not pulling its weight in my opinion. Two protocols (FilterStoreManager
and FilterStore
) and three classes (FilterStoreManager
, HCIRNFilterStore
, MachadoFilterStore
) could easily be merged into a single class. A single class with the responsibility of transforming a (VisionType, Simulation)
pair into a CIFilter
. (Note: keeping the *Vendor
classes separate is a good idea though.)
I'll admit I'm also guilty sometime of writing too much structure to encapsulate simple tasks. I guess it's a natural thing to do when trying to split a problem domain into its components: you sometime end up with too many components. What happens later if you keep them is it makes future changes to the code more complicated because some responsibility is scattered through a sea of components.
Most of your PR and the features it adds is quite good, that's why I'm putting extra efforts into reviewing it. Please keep going. I'll probably have more comments later about smaller things (it's a big PR after all). Feel free to ask if you have questions or want some clarification.
} | ||
} | ||
|
||
fileprivate enum VisionTypeDTO: Int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand the point of this enum. It has all the same types as VisionType
, why use a different enum?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, it is a perhaps unnecessary habit of isolating persisted models from app models (i.e., the rawValue Int). It can simply use VisionType / Simulation and a persistenceValue
so it is at least explicit what a change could disrupt.
static let SimulationKey = "SimulationKey" | ||
} | ||
|
||
fileprivate enum SimulationDTO: Int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same with this one. Why not use Simulation
?
guard unhideOnNextDisplay else { return } | ||
DispatchQueue.main.async { [weak self] in | ||
// recheck, because the state could have changed since the dispatch | ||
guard self?.unhideOnNextDisplay == true else { return } | ||
self?.unhideOnNextDisplay = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's a thread safety issue here, although not of your own making since the same logic is present in the older code: unhideOnNextDisplay
is used from both the main queue and the capturing queue. Added #23 to track this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. This commit moves the main queue write to the filter queue.
Mac App/Model/Errors.swift
Outdated
alertHelpButton: "Contact the developer about this error." | ||
) as Error | ||
|
||
let AVScreenCaptureSessionSetupFailure = NSError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I mistaken or is this error constant not used anywhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for catching that. It was a stub for adding a screen recording permissions prompt for 10.15+. This commit:
- presents an error from CGWindowListCapturer with an adaptive image
- adds translations for the Metal errors
One class now controls vision type changes and swapping filter vendors. A CIFilterVendor uses a stringly-typed method to create CIFilters. To simplify how the new FilterStore would call for filters, the VisionType enum has been extended with standardized CIFilter names.
052f681
to
f086d0a
Compare
As a self-taught developer, I welcome all the feedback you've given. Thank you for all your comments. It's fun to contribute.
Agreed. Condensing them removed a lot of boilerplate. The only differences really were in the Vendors. |
I've left this hanging for too long, sorry. I think I'll accept this as is and perhaps make a few changes myself later if needed. Can you just remove or revert 7bf5c79?, I'm not sure this fix is correct and I'd rather address it separately. (It's not like you introduced that bug, it was there before too.) |
…lay to a single queue" This reverts commit 7bf5c79.
Reverted. I've also meant to come back to this. Thanks for the prompt. |
Merged! Again, thank you. I look forward to other pull requests. I made some changes (rationale in each commit message):
I also believe there's too much code in UserDefaults+WindowRestoration.swift for what it does, but I'm not sure what to do yet with this. I suppose migrating this to iOS might help figure out how to streamline this. |
As discussed via email, I'll continue to look at CPU-side timing to boost performance for larger window capture areas from this starting point. Monitoring the preview window's screen membership to switch preferred Metal devices might also be better.
Finally, sorry re: using one commit for the Filter menu additions and Mandarin translations; hopefully it's still ok to sort through.