Skip to content
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

Merged
merged 37 commits into from
Oct 29, 2021

Conversation

importRyan
Copy link
Contributor

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.

- 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
Copy link
Owner

@michelf michelf left a 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;
Copy link
Owner

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.

Copy link
Contributor Author

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
Copy link
Owner

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:

  1. the border is larger and more visible, which helps make the window visible given that discounting the shadow effect it is otherwise transparent
  2. 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)
Copy link
Owner

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) {
Copy link
Owner

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.

  1. It's not needed in Swift: it's always equivalent to Int. In fact, all Apple APIs expose NSInteger as Int in Swift.
  2. 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 an enum that I am overlooking?

Copy link
Contributor Author

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.

self.visionSimulation = vision
DispatchQueue.global().async { [weak self] in
self?.visionFilter = self?.loadFilter(for: vision)
}
Copy link
Owner

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.

Copy link
Contributor Author

@importRyan importRyan Aug 3, 2021

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.


/// 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
Copy link
Owner

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.)

@michelf
Copy link
Owner

michelf commented Aug 2, 2021

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.

@importRyan
Copy link
Contributor Author

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.

@importRyan
Copy link
Contributor Author

@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.

@michelf
Copy link
Owner

michelf commented Aug 12, 2021

Thank you for those changes. I should be able to review this next weekend.

Copy link
Owner

@michelf michelf left a 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 FilterStores 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 {
Copy link
Owner

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?

Copy link
Contributor Author

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 {
Copy link
Owner

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?

Comment on lines +136 to +140
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
Copy link
Owner

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.

Copy link
Contributor Author

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.

alertHelpButton: "Contact the developer about this error."
) as Error

let AVScreenCaptureSessionSetupFailure = NSError(
Copy link
Owner

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?

Copy link
Contributor Author

@importRyan importRyan Aug 24, 2021

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.
@importRyan
Copy link
Contributor Author

Most of your PR and the features it adds is quite good, that's why I'm putting extra efforts into reviewing it.

As a self-taught developer, I welcome all the feedback you've given. Thank you for all your comments. It's fun to contribute.

when trying to split a problem domain into its components: you sometime end up with too many components.

Agreed. Condensing them removed a lot of boilerplate. The only differences really were in the Vendors.

@michelf
Copy link
Owner

michelf commented Oct 27, 2021

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.)

@importRyan
Copy link
Contributor Author

Reverted. I've also meant to come back to this. Thanks for the prompt.

@michelf michelf merged commit 02eb29c into michelf:master Oct 29, 2021
@michelf
Copy link
Owner

michelf commented Oct 29, 2021

Merged! Again, thank you. I look forward to other pull requests.

I made some changes (rationale in each commit message):

  • b68ae39: Merged ThreadSafeFilterStore and FitlerStore in one class
  • 628937d: Renamed ScreenCapturer protocol to ImageCapturer
  • b7e4c9e: Moving Simulation and VisionType enums out of the Mac folder

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants