Bloc icon indicating copy to clipboard operation
Bloc copied to clipboard

having different compositing layer render element in the wrong order.

Open rvillemeur opened this issue 1 year ago • 5 comments

Take this code. The intent is the have the shadow of the backdrop behind the front element.

container:= BlElement new geometry: BlRectangleGeometry new; size: 150@150; background: Color lightGray .
        
backdrop:= BlElement new geometry: BlCircleGeometry new;  size: 100 asPoint;
	effect: (BlGaussianShadowEffect color: Color black   offset: 0@ 0  width: 80);
             background: Color black.
            
front:= BlElement new geometry: BlCircleGeometry new; size: 100 asPoint; position:5@5; background: Color red.
        
container addChild: backdrop.
container addChild: front.

container.

However, in current Bloc, we get this image

Instead of expected effect: image

Reason is: In

aeFullDrawOn: aCanvas
	"Main entry point to draw myself and my children on an Alexandrie canvas."
	self aeDrawInSameLayerOn: aCanvas.
	self aeCompositionLayersSortedByElevationDo: [ :each | each paintOn: aCanvas ].

we call:

aeCompositionLayersSortedByElevationDo: aBlock

	(self isTransparent or: [ self isVisible not ])
		ifTrue: [ ^ self ].

	self wantsSeparateCompositingLayer ifTrue: [
		self hasCompositionLayer ifFalse: [
			self compositionLayer:
				(BAAxisAlignedCompositionLayer newFor: self) ].
		aBlock value: self compositionLayer ].

From BAAxisAlignedCompositionLayer comment: Basically, when an element has to draw itself on a canvas and responds true to wantsSeparateCompositingLayer, then it's drawn in a new Cairo surface, and then "painted" into the host space canvas. Next pulse with drawing phase needed, if the element wasn't invalidated, the Cairo surface will be painted again into the host space canvas without need to redraw again. Element effect's have wantsCompositionLayer on their API, which the element queries to decide if it has a composition layer. For example, BlGaussianShadowEffect answers true.

In current situation, when you apply effect: to an element, the order of element is not properly managed.

If you change BlGaussianShadowEffect >> wantsCompositionLayer to return ^ false, you get the desired effect. However, I doubt this is the correct fix 😥

rvillemeur avatar Jun 26 '24 21:06 rvillemeur

This shows a bug when the composition layer is enabled, so you gave a good workaround until it is fixed (making BlGaussianShadowEffect >> wantsCompositionLayer to return false)

tinchodias avatar Jun 26 '24 21:06 tinchodias

Maybe duplicated from https://github.com/pharo-graphics/Bloc/issues/342 but this is a better report

tinchodias avatar Jun 26 '24 22:06 tinchodias

I pushed @rvillemeur's workaround. This issue can stay open to remember to fix it.

tinchodias avatar Jul 04 '24 14:07 tinchodias

Note for later - as of august 2024, current render loops look like:

It start with

BASpaceRenderer >> renderSpace: aBlSpace
	aBlSpace aeFullDrawOn: aeCanvas.

BlSpace default root being a BlElement, the full list of call to AeDraw* function which are called follow this order:

BlElement >> aeFullDrawOn: aCanvas
	BlElement >> aeDrawInSameLayerOn: aCanvas.
		BlElement >> aeDrawOn: aeCanvas
		BlElement >> aeDrawIgnoringOpacityAndTransformationOn: aeCanvas
			BlElement >> aeDrawEffectBelowGeometryOn: aeCanvas.
			BlElement >> aeDrawGeometryOn: aeCanvas.
			BlElement >> aeDrawEffectAboveGeometryOn: aeCanvas.
			BlElement >> aeDrawChildrenOn: aeCanvas. "Z-index: children sorted by elevation"
				BlElement >> aeDrawInSameLayerOn: aCanvas "and recursive repeat"
	BlElement >> aeCompositionLayersSortedByElevationDo: [ :each | each paintOn: aCanvas ].

BlElement >> aeCompositionLayersSortedByElevationDo: call

	BAAxisAlignedCompositionLayer >> paintOn: aCanvas
	BAAxisAlignedCompositionLayer>> ensureReadyToPaintOn: aCanvas
		BlElement >> aeDrawAsSeparatedLayerOn: layerCanvas
			BlElement >> aeDrawIgnoringOpacityAndTransformationOn: layerCanvas

At one point, both end calling aeDrawIgnoringOpacityAndTransformationOn. The logic here is to separate rendering into different layer (Z-index is called elevation). Composition layer is mostly a cached rendering, used when the underlying element doesn't need frequent refresh (Identified use case are: drawing a graph, element moving in a drag&drop, shadow gaussian effect).

Potential solution could be to merge Layer rendering into one global loop

rvillemeur avatar Aug 06 '24 20:08 rvillemeur

@tinchodias looking at the drawing logic used, I updated Bloc code to:

BlElement >>aeFullDrawOn: aCanvas
	"Main entry point to draw myself and my children on an Alexandrie canvas."
	self aeDrawInSameLayerOn: aCanvas.
	"self aeCompositionLayersSortedByElevationDo: [ :each | each paintOn: aCanvas ]"

and updated aeDrawInSameLayerOn: aCanvas to include composition layer.

BlElement >> aeDrawInSameLayerOn: aCanvas
	(self isTransparent not and: [
		 self isVisible and: [ self wantsSeparateCompositingLayer not ] ])
		ifTrue: [ self aeDrawOn: aCanvas ]
		ifFalse: [
			self hasCompositionLayer ifFalse: [
				self compositionLayer:
					(BAAxisAlignedCompositionLayer newFor: self) ].
			self compositionLayer paintOn: aCanvas ]

I can see it fix the issue. I also run code you mention at https://discord.com/channels/223421264751099906/375240886319316994/1301051442030706772, and can see performance improvement as well. I'm wondering what I'm missing with this solution. Any hint ?

Code need to be refactored for a complete solution, but I would like your opinion first. To explain further my change. In current logic, we first draw all element that are not in a composition layer. Once its done, we then draw element in composition layer, when then show on top of the rendering. Those steps shows on below, and this is how we have the bug.

BlElement >> aeFullDrawOn: aCanvas
	BlElement >> aeDrawInSameLayerOn: aCanvas.
		BlElement >> aeDrawOn: aeCanvas
		BlElement >> aeDrawIgnoringOpacityAndTransformationOn: aeCanvas
			BlElement >> aeDrawEffectBelowGeometryOn: aeCanvas.
			BlElement >> aeDrawGeometryOn: aeCanvas.
			BlElement >> aeDrawEffectAboveGeometryOn: aeCanvas.
			BlElement >> aeDrawChildrenOn: aeCanvas. "Z-index: children sorted by elevation"
				BlElement >> aeDrawInSameLayerOn: aCanvas "and recursive repeat"
	BlElement >> aeCompositionLayersSortedByElevationDo: [ :each | each paintOn: aCanvas ].

However, if we look at BlElement >>aeDrawInSameLayerOn: and BlElement >> aeCompositionLayersSortedByElevationDo: we can notice they begin with similar logic.

BlElement >> aeDrawInSameLayerOn: aCanvas
	(self isTransparent not and: [
	self isVisible and: [
	self wantsSeparateCompositingLayer not ] ])
		ifTrue: [ self aeDrawOn: aCanvas ]
BlElement >> aeCompositionLayersSortedByElevationDo: 
	(self isTransparent or: [ self isVisible not ])
		ifTrue: [ ^ self ].
	self wantsSeparateCompositingLayer ifTrue: [
		self hasCompositionLayer ifFalse: [
			self compositionLayer:
				(BAAxisAlignedCompositionLayer newFor: self) ].
		aBlock value: self compositionLayer ].

Code in aeCompositionLayersSortedByElevationDo: look for element in composition layer, compute them and cache them if its not already done, and later loop on element children. This loop is already in place for regular element. By Adding the compute of composition layer in the same loop, we keep them cache if needed, while having them place in the correct order in the children hierarchy.

rvillemeur avatar Oct 30 '24 15:10 rvillemeur