Texture bleeding - Blitzmax bug with virtual resolution

Started by Derron, October 13, 2017, 21:59:44

Previous topic - Next topic

Derron

Just a short question to you all:


I saw a let's play of my game and the user seems to have issues with my sprite atlases: pink lines are visible (surrounding sprites in my atlases) - or black ones (ninepatch sprites).
(if you want to see it live: https://  [...] youtu.be/0tipTUY_JaI?list=PLXsYWPpDygXUpyERvNJYxNJgnNTXL2xEs - or download it on your own at http://www.tvtower.org)
)


I assume it is happening on a windows machine - using OGL (with DX he does not have this issues). On my machines neither OGL nor DX have this issue. My Sprites are _not_ power-of-two. Would this already solve the issue? Should I consider creating them "live on load"? Will save some GPU-memory when not doing that.




How do you tackle that?




PS: Sprite atlases are used to improve render speed - and I think I would have some hundreds to thousands of sprites if doing them "separately".

PPS: Excuse the youtube-link but I do not know how to "disable" autoconversion of youtubelinks to "video embed".



Thanks in advance for your ideas and tips.




bye
Ron

Kippykip

Can't say I've ever had this happen before, although I'm only using the built in Max2D functions.
I should also mention I've only used the OpenGL rendering modes on the non-NG version of BlitzMax. But I didn't have any issues with it.

Blitz3D on the other hand using fast image, I ran into this issue a lot and solved it partially by making everything by the power of two.

Derron

I also only use the built-in functionality. Especially "DrawImageSubRect()" to draw portions of an image -> sprite atlas. It also happens to "non-stretched / non-repeated"-images, so it seems to be not a problem of the texture itself but some kind of coordinate mismatch or stretching of the whole texture.




Let's Player reported issue to be only on OGL not in DX. Luckily my "LoadImage" is encapsulated already in a "RegistryLoader"-class so it should be possible to resize the textures accordingly when loading on game start. But for now I am still open for discussion about other potential flaws resulting in this texture bleeding.




bye
Ron

col

At a guess my thoughts are steering towards the texture sampler. Is it possible to space the textures out another pixel or 2? That may help.
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

TomToad

Had a similar problem before.  Turns out that texture filtering (anti-aliasing, MIP maping, etc...) was being done on the entire sprite sheet causing portions of one sprite to bleed into the next.  Solution was to make sure at least a 1 pixel border surrounded each sprite.
------------------------------------------------
8 rabbits equals 1 rabbyte.

Derron

#5
Instead of bleeding (mixing in outside pixel's colors) it is more or less a problem of "displacement".





The right side of the "red surrounded area" is using a stretched sprite of the atlas, the left side is using another sprite + an overlayed icon. As you see, the "bottom" is flawless while the "top" seems to have an offset of 1 pixel. This is not the case on my machines and also no other user complained yet so I think it is no issue of my "sprite position calculations" (talking of floating point determinism ;-)).




As you might see, it also does not happen to all sprites:





The sprite atlas is this one:

(the black lines are markers for my nine-patch code - so they define stretchable areas and content padding).




If the engine scaled the texture up to the next power of two - why should it "offset" textures? "Bleeding" is merging in other colors" - it still might be the case here but after some closer looks I would more tend to say it is some "offset" problem.




If you want to check if it happens on your computer too (if so it might be more probably that the error is in my framework - see "floating point determinism"):
https://github.com/GWRon/Dig
especially the sample:
https://github.com/GWRon/Dig/tree/master/samples/gui
(as it loads some of the gui-widgets based on the same priciples - sprite atlases).



Edit: having a closer look at aboves screenshots: if the texture "displacement" was a generic problem, then the whole "green bar" would have been displaced - so both should be "off by one pixel" but as this happens to only the stretched area in that case, it might be an issue with "DrawSubImageRect()" (and maybe the given parameters). But if that was the case: why does it happen to other sprites (non stretched, just using the absolute atlas positions) too?





Ok so, let's assume it is a flaw in my code (the calculation of sub-texture offsets): shouldn't it then happen in all cases, OGL _and_ DX (as both get passed "x=101" instead of "x=100" to their internal draw/render-commands). As the player told me it only happens on "OGL" I tend to say it is some texture filtering thing or so.



bye
Ron

col

Oh my... this will be a trial and error thi g to resolve  ;D
If you're 100% sure that your math is correct then suspect the texture sample settings. Sampling can happen across more than the immediate adjacent 1 pixel dependong on the sampling mode. If you try moving the image down maybe 1, 2 or 3 pixels to test if sampling occurs o
at a different place on the texture, maybe turn mip mapping off.

It doesnt help that you cant reproduce it so it'll be like finding a needle in a haystack.
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

Derron

#7
But how are "sampling settings" and "x offset = 1" and/or "y offset = 1" connected? If it was a incorrect calculation it should happen with DX and OGL. If the textures are somehow "resized/scaled" (a little bit - during GPU-wise-powerOfTwo-correction) then why does it affect nearly all of these "multiple-sprite-compositions" (with exactly only the "stretchable" portion of the whole texture being displaced).


Might there be issues in Max2D regarding this? So some problems when passing floats to them? It all puzzles me because a "source code flaw" should be visible on one of my machines (I assume so) and a driver/gpu-issue should not create "offsets". Argh.




Did you run the DIG/sample code (should work with NG and vanilla - as we tried that on android as a somewhat more "complex" example :-)) ?




Regarding DrawSubImageRect():

Function DrawSubImageRect( image:TImage,x#,y#,w#,h#,sx#,sy#,sw#,sh#,hx#=0,hy#=0,frame=0 )
   Local x0#=-hx*w/sw,x1#=x0+w
   Local y0#=-hy*h/sh,y1#=y0+h
   Local iframe:TImageFrame=image.Frame(frame)
   If iframe iframe.Draw x0,y0,x1,y1,x+gc.origin_x,y+gc.origin_y,sx,sy,sw,sh
End Function

Source: https://github.com/blitz-research/blitzmax/blob/master/mod/brl.mod/max2d.mod/max2d.bmx


It happens to use "floats" as params, so I am passing calculations like "area.GetW() - 2 * NINEPATCH_MARKER_WIDTH" to it.


Might it be, that these values result eg. in "15.999997 - 2 * 1", so "13.999997" (or 14.000002) and then somehow the internal command (GL, DX) round it somewhat (eg. to "13" or "14.000002 to 15") ?
BUT - if that really was happening (rounding of values) then shouldn't these "offsets" happen in one run of the game, and in the other run it should not happen for that sprite? I mean: the "floating point determinism"-problem should result in a "random" value on calculation - so each "run" should result in a different value (rather than a precalculated value stored in the binary).






I also thought on creating a sample binary which exposes the texture used there: so one could check on a screenshot then, if the texture was altered. Or couldn't I already do that in a way of using GrabPixmap and then checking the pixmap data against the texture-pixmap-data (ignoring alpha) - as I assume, the pixmap-data is "unaltered" (compared to the TGLImageFrame and the likes). So I could automatically print "OK" or "failed" on certain tests.






Bye
Ron

col

Its surprising what happens from cpu to gpu, and even between gpu stages. At where you are now I would install a gpu diagnostic tool and take a look at what the gpu is doing. I use RenderDoc myself as it supports the later APIs but unfortunately it doesnt work with <gl3.2. With a gpu debugger you can get rid of all the guess work and examine the complete pipeline state at various stages of rendering. Googling 'opengl gpu debugger' brings up some potential tools that you could use. Ati/Amd and Nvidia do their own tools too.

https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

Derron

That would be a solution if that issue was existent on my computer too - but I cannot convince a normal end-user to install a tool as described. So solutions are, to write a programm which does similar things but allows eg. to use the same texture but pre-resized to power-of-two or to use hardcoded coordinates for sprites and the likes.


This is why I am asking for further "sources" of this problem - what else could create that "bugged" behaviour?






bye
Ron

col

#10
Yep, I only meant for you to install the tool.
It may still show up the problem even if the final image gets compensated somehow - you will see the values sent from the cpu to gpu - so you can compare these. You can debug single vertices and single pixels all the way from entering the pipeline to being rastered to the render target. If anything looks little off if will be a direction to investigate further - as opposed to having no clue what so ever.

I'll have a go at installing one and see if anything shows up...

Can you create a working sample code with an image of how you're doing things for me to test?
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

Derron

@ Sample


Either download my game (so you can at least have a nice little play afterwards :-)). The issue if apparent would be already visible in the start screen (GUI elements).


Also you could use the DIG-framework and the GUI-Sample (as it uses gui widgets...[sic!]) or the demo-app, doing similar stuff.




Regarding a specific application:
- so drawing the spritesheet
- drawing eg. a gui button
?


What platform should I compile for? Linux32, Linux64(with NG), Mac32 or Windows32 ?




bye
Ron

Derron

#12
Ok - so instead of sending you a binary (with unknown content ;-)):

- Download my Dig-Framework (contains the GUI stuff etc) and extract it:
https://github.com/GWRon/Dig

- Copy this code into a new file and save it as "DIG/samples/gui/spritetest.bmx":

SuperStrict

'keep it small
Framework BRL.standardIO
Import "../../base.framework.graphicalapp.bmx"
Import "../../base.util.registry.bitmapfontloader.bmx"
Import "../../base.gfx.gui.button.bmx"


Global MyApp:TMyApp = New TMyApp
MyApp.debugLevel = 1


'kickoff
MyApp.SetTitle("Sprite Test")
MyApp.Run()
End




'-------------------

Type TMyApp Extends TGraphicalApp
   Field mouseCursorState:Int = 0
   Field guiSystemState:TLowerString = TLowerString.Create("SYSTEM")


   Method Prepare:Int()
      Super.Prepare()

      Local gm:TGraphicsManager = TGraphicsManager.GetInstance()
      'manually set renderer
      'gm.SetRenderer(TGraphicsManager.RENDERER_OPENGL)
      'gm.SetRenderer(TGraphicsManager.RENDERER_DIRECTX9)
      'gm.SetRenderer(TGraphicsManager.RENDERER_DIRECTX11)
   
      'scale everything from 800x600 to 1024x768
      'gm.SetResolution(1024, 768)
      'gm.SetDesignedResolution(800, 600)
      'gm.InitGraphics()

      '30 logic updates, unlimited FPS
      GetDeltatimer().Init(30, -1)
      GetGraphicsManager().SetVsync(False)
      GetGraphicsManager().SetResolution(800,600)

      'we use a full screen background - so no cls needed
      autoCls = True

      '=== LOAD RESOURCES ===
      Local registryLoader:TRegistryLoader = New TRegistryLoader
      'afterwards we can display background images and cursors
      '"TRUE" indicates that the content has to get loaded immediately
      registryLoader.LoadFromXML("res/config/startup.xml", True)
      'load this "deferred"
      registryLoader.LoadFromXML("res/config/resources.xml")

      '=== CREATE DEMO SCREENS ===
      GetScreenManager().Set(New TTestScreen.Init("page1"))
      GetScreenManager().SetCurrent( GetScreenManager().Get("page1") )
   End Method


   Method Update:Int()
      'fetch and cache mouse and keyboard states for this cycle
      GUIManager.StartUpdates()

      '=== UPDATE GUI ===
      'system wide gui elements
      GuiManager.Update( guiSystemState )

      'run parental update (screen handling)
      Super.Update()

      'check if new resources have to get loaded
      TRegistryUnloadedResourceCollection.GetInstance().Update()

      'reset modal window states
      GUIManager.EndUpdates()
   End Method


   Method Render:Int()
      Super.Render()
   End Method


   Method RenderContent:Int()
      '=== RENDER GUI ===
      'system wide gui elements
      GuiManager.Draw( guiSystemState )
   End Method


   Method RenderLoadingResourcesInformation:Int()
      'do nothing if there is nothing to load
      If TRegistryUnloadedResourceCollection.GetInstance().FinishedLoading() Then Return True

      'reduce instance requests
      Local RURC:TRegistryUnloadedResourceCollection = TRegistryUnloadedResourceCollection.GetInstance()

      SetAlpha 0.2
      SetColor 50,0,0
      DrawRect(0, GraphicsHeight() - 20, GraphicsWidth(), 20)
      SetAlpha 1.0
      SetColor 255,255,255
      DrawText("Loading: "+RURC.loadedCount+"/"+RURC.toLoadCount+"  "+String(RURC.loadedLog.Last()), 0, 580)
   End Method


   Method RenderHUD:Int()
      '=== DRAW RESOURCEL LOADING INFORMATION ===
      'if there is a resource loading currently - display information
      RenderLoadingResourcesInformation()

      '=== DRAW MOUSE CURSOR ===
      GetSpriteFromRegistry("gfx_mousecursor"+mouseCursorState).Draw(MouseManager.x, MouseManager.y, 0)
   End Method
End Type






Type TTestScreen Extends TScreen
   Field guiState:TLowerString


   Method Setup:Int()
      GuiManager.SetDefaultFont( GetBitmapFontManager().Get("Default", 14) )
      'buttons get a bold font
      TGUIButton.SetTypeFont( GetBitmapFontManager().Get("Default", 14, BOLDFONT) )
     
      Local guiRowX1:Int = 70

      Local button:TGUIButton = New TGUIButton.Create(New TVec2D.Init(guiRowX1, 20), New TVec2D.Init(130,-1), "Sample Button", Self.GetName())
      button.SetTooltip( New TGUITooltipBase.Initialize("A button click", "Clicking on a button might lead to a longer life...might!", New TRectangle.Init(0,0,250,-1)) )

      'register demo click listener - only listen to click events of
      'the "button" created above
      'EventManager.RegisterListenerFunction("guiobject.onclick", onClickMyButton, button)
      'EventManager.RegisterListenerFunction("guiobject.onclick", onClickAGuiElement)
      'EventManager.RegisterListenerFunction("guiobject.onclick", onClickOnAButton, "tguibutton")
   End Method


   Function onClickAGuiElement:Int(triggerEvent:TEventBase)
      Local obj:TGUIObject = TGUIObject(triggerEvent.GetSender())
      Print "a gui element of type "+ obj.GetClassName() + " was clicked"
   End Function


   Function onClickOnAButton:Int(triggerEvent:TEventBase)
      'sender in this case is the gui object
      'cast as button to see if it is a button (or extends from one)
      Local button:TGUIButton = TGuiButton(triggerEvent.GetSender())
      'not interested in other widgets
      If Not button Then Return False

      Local mouseButton:Int = triggerEvent.GetData().GetInt("button")
      Print "a TGUIButton just got clicked with mouse button "+mouseButton
   End Function


   Function onClickMyButton:Int(triggerEvent:TEventBase)
      'sender in this case is the gui object
      'cast as button to see if it is a button (or extends from one)
      Local button:TGUIButton = TGuiButton(triggerEvent.GetSender())
      'not interested in other widgets
      If Not button Then Return False

      Local mouseButton:Int = triggerEvent.GetData().GetInt("button")
      Print "my button just got clicked with mouse button "+mouseButton
   End Function


   Method Update:Int()
      If Not guiState Then guiState = TLowerString.Create(Self.name)
      GuiManager.Update(guiState)
   End Method
   

   Method Render:Int()
      'draw a background
      SetColor(255,255,255)
      GetSpriteFromRegistry("gfx_startscreen").Draw(0,0)

      Local centerY:Int = 5
      GetBitmapFont("default").DrawBlock("Button:", 10, 20 + centerY, 60, 35)
      GetBitmapFont("default").DrawBlock("Spritesheet:", 10, 60 + centerY, 60, 35)
     
      'draw the raw spritesheet
      GetSpritePackFromRegistry("gfx_guiTest").Render( 70, 60 + centerY)


      'draw button and tooltip of hovered button
      If Not guiState Then guiState = TLowerString.Create(Self.name)
      GuiManager.Draw( guiState )
     
   End Method
End Type


Now it could reuse existing code (configuration files) and should do similar stuff than my game (the TScreen-stuff is handled differently in my game but this should not bork stuff).


On my linux computer it looks like this:


But on the users computer the button contained it's left "nine patch marker"-black-line when hovering - and things like the tooltip (hover over the button and wait) would expose an offset of the top-border in its "stretched area".


PS: Interesting side note:
Using custom TTFs fonts via "Freetype" results in heavier (= more "bold") fonts on Windows. I load the fonts into a sprite atlas to render from them (to speed up rendering compared to the single-texture-per-glyph approach of vanilla). On windows my "bold" fonts look bigger than on linux.
But texts on the players "let's play"-videos seem to look unaffected from my "DrawSubImageRect()"-calls (so they are not oddly placed or so). Interesting for us is now: my bitmap class function and especially the texture atlas creation there is using a "power of two"-texture. An indicator of the issue? Or just luck that there is automatic "space between chars" even if I do not pad them manually.


To see how this texture looks: append the following code to the "Method Render:Int()"-portion:



If KeyManager.IsDown(KEY_SPACE)
SetColor 0,0,0
DrawRect(0,0,400,400)
SetColor 255,255,255

GetBitmapFont("default").spriteSet.Render(0,0)
EndIf





bye
Ron

col

Excellent, I'll give a go.

I've downloaded GpuPerfStudio as it supports >=OpenGL 1.0.
It looks like it will be help to verify the numbers.
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

col

The gui apps don't build with a 'TIntMap' type not found error.
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."