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

Re-work the managed-native types #900

Merged
merged 28 commits into from
Jul 30, 2019
Merged

Re-work the managed-native types #900

merged 28 commits into from
Jul 30, 2019

Conversation

mattleibow
Copy link
Contributor

@mattleibow mattleibow commented Jul 1, 2019

Description of Change

  • Added GCHandleProxy to debug builds
    • this is used to track all GCHandle Alloc and Free calls to ensure that all allocations are freed.
    • added some unit tests to make sure this is actually enforced
    • as a result, several object are now freed correctly
  • Added ISKReferenceCounted and ISKNonVirtualReferenceCounted interfaces to represent the reference counting types used in the native library
    • this helps with automatically de-referencing objects
  • SKAbstractManagedStream, SKAbstractManagedWStream and SKDrawable have been re-written to use better delegates
    • instead of passing each of the delegates as parameters, they are now a struct that is passed as a single object
    • better for extensions (which there shouldn't be) and only a single static field on the type
    • removed the usage of Marshal.GetFunctionPointerForDelegate, which should help out with WASM (see [FEATURE] Add a SkiaApi and SKAbstractManagedStream native abstraction #876)
    • the objects now only keep weak references, meaning that they can now be garbage collected
    • instead of trying to resolve the instances with a dictionary, a delegate is used and passed as "user context"
  • Moved some of the repetitive logic from the types into the base SKObject and SKNativeObject
    • some logic is automatically executed if the concrete type is ISKReferenceCounted or ISKNonVirtualReferenceCounted
    • with the more centralized logic and stricter patterns, better tests can be written to make sure all memory is freed correctly and timely
  • SKData, SKFontManager and SKTypeface now correctly prevent disposal of the "static" instances
  • SKPaint now references the Shader, MaskFilter, ColorFilter, ImageFilter, Typeface and PathEffect properties
    • this prevents accidental collection, or non-collection when the object goes out of scope
  • the SKPath iterators (Iterator and RawIterator) and op builder (OpBuilder) now correctly own and dispose their native objects
  • SKRegion objects are now disposed on the native side
  • SKTypeface construction from a SKManagedStream (via both SKTypeface and SKFontManager) now copy the contents of the .NET Stream into a native memory
    • typeface construction requires multiple seeks (previously, the stream was copied only if it was non-seekable)
    • it also requires "duplicating" the stream, which is not supported on .NET streams
      • duplicates or forks of a stream means that each of the streams need to be read concurrently from different locations
      • .NET streams can only have a single position
  • Updated the NuGets used for the tests
    • using the Xunit.AssemblyFixture and Xunit.SkippableFact NuGets instead of using the code directly
    • removed the Xunit.Categories NuGet as it was preventing tests from running

Bugs Fixed

Provide links to issues here. Ensure that a GitHub issue was created for your feature or bug fix before sending PR.

API Changes
None visible.

Behavioral Changes

This PR has a big set of changes that may be breaking due to bug fixes:

The SKAbstractManagedStream, SKAbstractManagedWStream and SKDrawable no longer prevent the GC from collecting them. This means that if code no longer references them, they will be disposed.

  • As far as I can tell, this should not be a problem for the streams as they are never kept around - they are just used for reading and writing and typically only need to live for as long as a single method, and then need to be disposed by the caller. The SKTypeface and SKDocument do keep it around for a bit, but then they also take ownership of the stream and keep a hard reference to the streams themselves. They will dispose the streams when they are disposed.
  • SKDrawable is never kept around and is entirely a user-controlled object. If it goes out of scope, skia doesn't have a reference anyway.

The SKFontManager and SKTypeface no longer use the managed streams (SKManagedStream or Stream) directly - they make a copy.

  • This is simply because skia streams can do things that are not possible for .NET - they can be read concurrently from different positions. If a SKFileStream or SKMemoryStream are passed, then the streams are not copied.
  • Further optimizations can be made in the case of a MemoryStream or byte[] to not actually copy but use GC pinning to get a handle to the managed data and work with pointers. But this can be done later so that this PR can be merged and tested.

PR Checklist

  • Has tests (if omitted, state reason in description)
  • Rebased on top of master at time of PR
  • Changes adhere to coding standard
  • Updated documentation

 - added several debug-only hooks to track objects and exceptions
 - added a field to keep the document stream alive
 - drawable picture is not referenced as it is a plain pointer
 - creating typefaces always copies the managed stream as multiple reads are needed
 - automatically dispose objects that already exist in the instances dictionary when the same handle is loaded
    - the object may already be deleted on the native side
    - a native crash will happen if the object is unref-ed later
@@ -78,11 +79,8 @@ public void ReleaseImagePixelsWasInvoked()
using (var image = SKImage.FromPixels(pixmap, onRelease, "RELEASING!"))
{
Assert.False(image.IsTextureBacked);
using (var raster = image.ToRasterImage())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer supported as it is not valid C#/.NET code. You have to actually dispose of the same SKImage instance twice (or more depending).If the instance is the same, you cannot dispose it more time.

{
public const float DefaultRasterDpi = 72.0f;

// keep the stream alive for as long as the document exists
private SKWStream underlyingStream;
Copy link
Contributor Author

@mattleibow mattleibow Jul 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The streams were in danger of being disposed while the document was in progress. To fix this, we keep a reference to it to make sure the GC doesn't pick it up early. This is different to the internal stream ownership where the internal streams are DISPOSED after usage. This will allow the stream to stay open and alive.

@mattleibow
Copy link
Contributor Author

mattleibow commented Jul 30, 2019

This PR has a big set of changes that may be breaking due to bug fixes:

The SKAbstractManagedStream, SKAbstractManagedWStream and SKDrawable no longer prevent the GC from collecting them. This means that if code no longer references them, they will be disposed.

  • As far as I can tell, this should not be a problem for the streams as they are never kept around - they are just used for reading and writing and typically only need to live for as long as a single method, and then need to be disposed by the caller. The SKTypeface and SKDocument do keep it around for a bit, but then they also take ownership of the stream and keep a hard reference to the streams themselves. They will dispose the streams when they are disposed.
  • SKDrawable is never kept around and is entirely a user-controlled object. If it goes out of scope, skia doesn't have a reference anyway.

The SKFontManager and SKTypeface no longer use the managed streams (SKManagedStream or Stream) directly - they make a copy.

  • This is simply because skia streams can do things that are not possible for .NET - they can be read concurrently from different positions. If a SKFileStream or SKMemoryStream are passed, then the streams are not copied.
  • Further optimizations can be made in the case of a MemoryStream or byte[] to not actually copy but use GC pinning to get a handle to the managed data and work with pointers. But this can be done later so that this PR can be merged and tested.

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.

1 participant