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

Replace BitmapPath with BitmapId #2632

Merged
merged 11 commits into from
May 18, 2024

Conversation

pauldendulk
Copy link
Member

No description provided.

@pauldendulk pauldendulk changed the base branch from main to feature/bitmapregistry May 13, 2024 09:36
@inforithmics
Copy link
Contributor

I had a rough look into this pull request and I like the changes being made. Especially that now the Images are cached by uri so that the uri is the id of the images/sprites/icons ..... I like the ImageFetcher too so there is a seperetion between loading and displaying like in the render Pipeline.

@@ -27,7 +27,7 @@ public void Dispose()
SymbolCache.Dispose();
VectorCache.Dispose();
TileCache.Dispose();
BitmapRegistry.Dispose();
ImagePathCache.Dispose();
Copy link
Member Author

Choose a reason for hiding this comment

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

Decided to rename from BitmapPath to ImagePath because svg is not a bitmap and already started to use this in some places.

btw, I am also considering a data:// schema to allow an svg string. Or maybe it should be svgstring:// or svg://. With this change ImagePath will also not be fully correct. ImageSource might be better.

Copy link
Member

Choose a reason for hiding this comment

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

The change is good, but ImagePath isn't good for this.

Regarding the correct naming: Up to now this property is used for the source (file, http and so on). You don't say anything about the content. So it should be ImageSource or Source.

Regarding SVG: When you add a svg: source, you combine the source with the type. That isn't logical. And a SVG could be an embedded resource, a file or coming via http. Also a string is possible. So a string and base64 source would be better. And there could be even more sources.

Mapsui core should be responsible for loading, the renderer for interpreting the content.

Copy link
Member Author

Choose a reason for hiding this comment

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

@charlenni Good point. All other schemes are source-types and svg is an image-type, so in that sense 'string' would be better, because we are using a string as source. But this would only work for svg and I would want to make that clear. So perhaps it should be svgstring.

And though it is always possible to put the svg in file or embedded resource it would be easier in some cases if you just could just copy and paste the string. Then again this functionality may not be that important. I'll sleep on it.


[return: NotNullIfNotNull(nameof(key))]
public SKImage GetOrCreatePaint(string key, Func<SKImage> toSKImage)
public T GetOrCreateSKObject<T>(string key, Func<T> toSKObject) where T : SKObject
Copy link
Member Author

Choose a reason for hiding this comment

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

Reusing SpriteCache to get CalloutRenderer to work. This is temporary and should be fixed before the next beta.

@@ -24,10 +23,10 @@ namespace Mapsui.Rendering.Skia;

public sealed class MapRenderer : IRenderer, IDisposable
{
private readonly DisposableWrapper<IRenderService> _renderService;
private readonly IRenderService _renderService;
Copy link
Member Author

Choose a reason for hiding this comment

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

We should put effort in trying to keep disposability simple. When possible there should be a single class responsible for creating and disposing object. In most cases this will be a cache.

I also would like to keep the render caches encapsulated within the render. They are performance optimizations of the a specific renderer and it would be better if there is no need to know anything about them on the outside.

{
_renderService = new DisposableWrapper<IRenderService>(new RenderService(), true);
// Todo: Think about an alternative to initialize. Perhaps the capacity should
Copy link
Member Author

Choose a reason for hiding this comment

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

User should not have to know how many features will be drawn by the renderer.

An alternative approach is used in the TileLayer. There all the tiles needed in a single iteration are always in cache. In the TileLayer we keep track of how many tiles are needed. MemoryCache.MinTiles is updated on every iteration.

A better solution may be to add the iterationCount parameter to cache methods so that the cache can know what is needed in a single render cycle.

if (calloutStyle.Invalidated)
{
// Todo: Move this update to the callout itself
calloutStyle.ContentId = Guid.NewGuid().ToString();
Copy link
Member Author

Choose a reason for hiding this comment

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

The calloutStyleRenderer heavily used the BitmapRegistry. It was tough to rewrite. The guid ids may have to go. Perhaps we should use an Id like in BaseFeature.

private static long NextId()
{
   return Interlocked.Increment(ref _currentFeatureId);
}

Another option is to keep the style immutable and you would have to assign a new style to change it.

Copy link
Member

Choose a reason for hiding this comment

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

It is, because, if you change anything in the callout content, you have to create a new callout to cache. It isn't logical to create a new style to change the header string or the font size of the detail entry.

Copy link
Member

Choose a reason for hiding this comment

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

Btw. your comment is wrong. The callout is cached, so that it has not to be created each time. It has nothing to do with the CalloutStyle. It is a CalloutStyleRenderer only thing.

@@ -18,7 +19,7 @@ public enum CalloutType
/// <summary>
/// Content is custom, the bitmap given in Content is shown
/// </summary>
Custom,
Image,
Copy link
Member Author

Choose a reason for hiding this comment

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

Rename CalloutType.Custom to Image because it is currently not possible anymore to use an in memory SKPicture as bitmap, just a BitmapPath image. Perhaps there should be no types at all just options to use an Image or Detail or not.

Copy link
Member

Choose a reason for hiding this comment

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

There are 3 types of callouts: a single line, a header detail combination and an user drawn.

Copy link
Member

Choose a reason for hiding this comment

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

If I think about the callout type, there could be a fourth type, an owner 'drawn' style. This provides two callbacks. One for getting the size of the content and one to draw the content. With this it should be possible to show normal UI elements in the callout. If you have to draw the callout, you get a screen position and a clip rect, where you have to display the normal UI elements. You need the clip rect when the callout is at the border of the map control.

@pauldendulk pauldendulk changed the title More work on BitmapPath and BitmapId Replace BitmapPath with BitmapId May 18, 2024
@pauldendulk
Copy link
Member Author

I had a rough look into this pull request and I like the changes being made. Especially that now the Images are cached by uri so that the uri is the id of the images/sprites/icons ..... I like the ImageFetcher too so there is a seperetion between loading and displaying like in the render Pipeline.

That is good to hear. It was one of the biggest changes I made in one PR and unfortunately I had to break a lot. I am convinced it will make things a lot better in the long run (it also allows some additional changes in upcoming PRs) but there will be frustrations because of breaking changes and I will have introduced some bugs.

About the ImageFetcher. Yes, this is the kind of thing I do a lot these days. I separate simple logic from stateful classes. The simple logic can often be pure functions. The ImageFetcher is not strictly a pure function because it depends on remote infrastructure but it is a method that is easy to understand. You put something in and something comes out. The ImagePathCache remains stateful, that is still a problem, but it is easier to see what it is about.

The current CalloutStyleRenderer also has some methods that could be moved out of that class to public pure functions so that they could be reused. The code to render the bubble and the tail (that is how the pointy part is called according to chatgpt) is quite advanced, not easy to write yourself and could be useful in other places. A custom renderer which creates an SKPicture could easily wrap that into the bubble to turn it into a callout using the RenderCallout method.

This separation of state and logic is what many developers think should be avoided because this seems to conflict with object oriented programming.

@pauldendulk
Copy link
Member Author

Summary, this PR replaced the SymbolStyle.BitmapId with a BitmapPath. It had a lot of impact on a lot of things and there are more changes to come. The BitmapRegistry (now the ImagePathCache) only holds streams that represent bitmap or svg images, not SKImage or SKPicture, these things should be in a cache specific to the SkiaRenderer.

@charlenni
Copy link
Member

As said in a code comment: ImagePathCache is totally useless in my eyes. You cache data, that no one needs. The renderer needs the image cache to draw the images fast. It converts the content in a format, that is capable of doing this. If the image changes, the ImagePath changes too. If the ImagePath doesn't change, the underlying converted renderable image doesn't change and the renderer hasn't to access the ImagePath a second time. So why to cache it? When is it useful to have a copy in memory? The only use case I could think about is, when you have two different renderers with two different caches for ready rendered ImagePathes.

@pauldendulk
Copy link
Member Author

pauldendulk commented May 18, 2024

There is still a lot more work to do on the caches but some general remarks about what I have in mind. I see three levels in our process:

  1. Fetch. Fetch data from a possibly remote source. In the layers this is the IProvider. In the styles this is the ImageFetcher. This logic is renderer independent and could fail because of external factor. The code for this could be in Mapsui core. We could test it without a dependency on the renderer.
  2. Render: Turn Mapsui features and styles into platform specific things like SKPicture and SKPaint. This is specific to the renderer and fully predictable. The code for this needs to be in the renderer.
  3. Draw: Draw the rendered features to the screen.

The ImagePathCache falls in the first layer and the and render cache in the second. This was a reason for me to treat is as a separate thing.

Something else I had in mind is that we could always keep the ImagePathCache in memory because I expect this to always be a limited set (unless users start generating file sources on the fly in code), while the render cache is cleaned regularly depending on what was recently in view or not. Then again, that depends on the implementation, we could also make a one on one cache for the skia version of the images permanently in the cache. So, that argument may not fly.

The main reason for doing it like this now is the logical organization that I have in mind. I am trying to organize it in a way that is easy to understand for me. Not sure if there is a real need for it in the end, but then it might still make more sense to organize it like that. Let's see how it turns out.

@charlenni
Copy link
Member

I'm not sure, if the ImagePathCache (btw. a misleading name) is needed. You say, that there are not so much things to fetch than to render. But that isn't correct. Each thing to render has to be fetched, but could be used more times. And there isn't a thing, that could be rendered without fetching it before. If a thing have the same source, then it should be already in the render cache. No need for accessing the fetch cache. And what should be the reason to remove a rendered thing from the render cache, but not from the fetch cache? How do you determine, that the thing should be removed from the render cache. It could come again in the next rendering because the map is moving the next symbol in.

So the question is, what could be done with fetched cache thing except transferring them to render cache? Tiles, symbols or brushes, all of them are used for rendering.

@pauldendulk pauldendulk merged commit f9f80c0 into feature/bitmapregistry May 18, 2024
5 checks passed
@pauldendulk pauldendulk deleted the feature/work-on-bitmapid branch May 18, 2024 14:11
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.

None yet

3 participants