Skip to content

OpenGL UI components#52

Open
morganchristiansson wants to merge 38 commits into
Return-To-The-Roots:masterfrom
morganchristiansson:gl2-ui
Open

OpenGL UI components#52
morganchristiansson wants to merge 38 commits into
Return-To-The-Roots:masterfrom
morganchristiansson:gl2-ui

Conversation

@morganchristiansson

Copy link
Copy Markdown
Contributor

Draw all UI components with OpenGL.
Keeps UI component SDL_Surface for a smaller PR but avoids 4K penalty of uploading a 4K 32bit 33MB surface each frame 2GB per second. Only small component surfaces are uploaded each frame.
Splash, main menu and options screens are entirely OpenGL.

Replacing SDL_Surface in each component with OpenGL to be addressed in future PR.
Terrain editor Surf_Map remains 2D/SDL/SGE in this branch.

@morganchristiansson

Copy link
Copy Markdown
Contributor Author

Also I can push this PR all the way replacing the per-component SDL_Surface with OpenGL. It's not hard at all with AI agent. I only left it out of scope for simpler review. Main benefit is already achieved.

@Flamefire

Copy link
Copy Markdown
Member

Draw all UI components with OpenGL.
Keeps UI component SDL_Surface for a smaller PR but avoids 4K penalty of uploading a 4K 32bit 33MB surface each frame 2GB per second. Only small component surfaces are uploaded each frame.
Splash, main menu and options screens are entirely OpenGL.

Can you give a bit more background? This code is old with old design decisions etc. which I don't remember especially as it wasn't written by me.
So a short summary of what and where issues are, how they manifest, what the change actually does and how that solves the problem.

IIRC SDL does use OpenGL as the backend/renderer, so the code alread is "using OpenGL" (to some extent)

@morganchristiansson

Copy link
Copy Markdown
Contributor Author

s25edit doesn't use OpenGL at all.
SDL and SGE is entirely 2D. Software CPU rendering.
Personally I get 1-2fps on 4k display so it's unusable. Idk if it's cause I have 13 year old 4.5GHz quad core i7-4770k CPU and DDR3 RAM and newer CPUs can easier push ~2GB of 32bit pixel data per second @ 60fps.

And say I want to use ctrlButton from libsiedler2 instead of s25edit CButton.
It draws with 3D https://github.com/Return-To-The-Roots/s25client/blob/master/libs/s25main/controls/ctrlButton.cpp#L92-L94

Keep small surface per UI component that is drawn with OpenGL
Avoids big 4K penalty while keeping change small
@morganchristiansson

morganchristiansson commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Hmm this PR is currently really small and nice.

But replacing getSurface() with getTexture() is keeping with 2D blitting patterns, s25client uses Draw() which enables components to draw directly. Much bigger PR tho.
I guess getTexture() is fine tho and gets us most value from least change.
But if Draw() is better than render loop doing getSurface().Draw(), and the UI components should not have both SDL_Surface and Texture, then should just eat the bigger PR now rather than having a follow up after.

And hmm maybe #47 gl2-terrain should really be parallel PR and not on top of this one. It doesn't change UI components at all.

@morganchristiansson

Copy link
Copy Markdown
Contributor Author

Ok I went ahead and implemented Draw() with removed getSurface() and getTexture()

# before:
12 files changed, 143 insertions(+), 78 deletions(-)
# after:
25 files changed, 632 insertions(+), 903 deletions(-)

bigger change but removing 280 lines!

@morganchristiansson morganchristiansson changed the title OpenGL UI (on top of #50 OpenGL backgrounds & cursor) OpenGL UI components Jul 3, 2026

@Flamefire Flamefire left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I admit this does simplify some things.

Added comments where AI overlooked stuff or we can be more succinct or consistent.

Also remove mentions like "uses OpenGL". That looks like AI added it because it changed it. Not because it is useful information at that point.
Especially given now most is rendered with OGL it would be noteworthy if something does not. But that is usually clear by e.g. passing a destination surface

Comment thread CIO/CButton.cpp Outdated
Comment thread Texture.cpp Outdated
Comment thread Texture.cpp Outdated
Comment thread Texture.cpp Outdated
Comment thread Texture.cpp Outdated
Comment thread CGame_Render.cpp Outdated
Comment thread CMap.cpp Outdated
Comment thread Texture.cpp Outdated
Comment thread Texture.cpp Outdated
Comment thread Texture.h Outdated
@morganchristiansson

Copy link
Copy Markdown
Contributor Author

59 comments! I reviewed them with the AI guiding me and grouping them.
I'm trying to not be a slop cannon!
Good stuff.

The AI might have missed something tho, cause you know, computers have shit memory and forget thing nowadays.

@morganchristiansson

Copy link
Copy Markdown
Contributor Author

I admit this does simplify some things.
Glad you like it!

But that is usually clear by e.g. passing a destination surface

There's two full screen SDL_Surfaces left:

  • Surf_Display is only UI components: windows, buttons, text inputs etc etc.
    It used to also have CMenu backgrounds.
  • Surf_Map is terrain editor, map objects, screen frame, even the icon buttons in the frame (which are not UI components).
    Surf_Map gets blitted to Surf_Display in master (with transparency I guess).

This branch gl2-ui deletes Surf_Display and makes Surf_Map the only full screen surface.
gl2-terrain #59 deletes Surf_Map.
Will need to resolve some minimal conflicts when one is merged but the branch changes do not overlap, much.
There shouldn't be any SDL_Surface left after these unless we missed some small internal surface.

Comment thread CIO/CControlContainer.cpp Outdated

@Flamefire Flamefire left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

But that is usually clear by e.g. passing a destination surface
There's two full screen SDL_Surfaces left:

yeah, just do a quick grep for comments and mentions of OpenGL/OGL on the diff to check for such issues please.

The AI might have missed something tho, cause you know, computers have shit memory and forget thing nowadays.

They are becoming too human 😆

Most things are (still) about using Position/Extent. I'm caring because we had subtle bugs where a C&P used e.g. "x" twice instead of "y". Those classes avoid such cases completely and make code (usually) more readable through the ops. They can even be combined like base types Position() + Extent() yields a Position, because one of those is signed which is usually what you want. Although it is not like int + unsigned yielding unsigned

Comment thread Texture.cpp Outdated
Comment thread Texture.cpp Outdated
Comment thread Texture.cpp Outdated
Comment thread Texture.cpp Outdated
{
static std::vector<std::unique_ptr<Texture>> cache;
static std::vector<bool> linearFlags;
if(idx < 0 || idx >= static_cast<int>(global::bmpArray.size()))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

want to do that (in a separate commit) or is that change to large?

Comment thread Texture.h Outdated
Comment thread CIO/CWindow.cpp Outdated
Comment thread CIO/CWindow.cpp Outdated
return false;
}
const Position origin = pos_;
const Rect winRect(origin, size_);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can use the method directly at the single place this is used, maybe also origin/getPos and using Position arithmetic

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

inlined origin and winRect

Comment thread CIO/CWindow.cpp
Comment thread CGame_Render.cpp Outdated
Comment thread Texture.cpp Outdated

@Flamefire Flamefire left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

few left-over from last round and some new after changes

Comment thread CIO/CWindow.cpp Outdated
}
// Clamp window position to screen bounds
const auto res = global::s2->getRes();
const int maxX = static_cast<int>(res.x - size_.x) - 1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just thinking about the cast: Do we ensure size < res? Otherwise we might need:
elMax(Extent::all(1), res - elMin(size_, res))
otherwise we have UB (large uint->int, lo>hi in clamp)

Comment thread CIO/CWindow.cpp Outdated
Comment on lines +36 to +38
{
return (global::s2->getRes() - size) / 2;
} else

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
{
return (global::s2->getRes() - size) / 2;
} else
return (global::s2->getRes() - size) / 2;
else

Comment thread CIO/CWindow.cpp Outdated
CSurface::Draw(surface, global::bmpArray[resizebutton].surface,
Position(w_, h_) - global::bmpArray[resizebutton].getSize());
getBmpTexture(resizebutton)
.draw(origin + Position(static_cast<int>(size_.x), static_cast<int>(size_.y))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
.draw(origin + Position(static_cast<int>(size_.x), static_cast<int>(size_.y))
.draw(origin + Position(size_))

Comment thread Texture.cpp Outdated
return *cache[idx];
}

void ensureBmpTex(int idx)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

?

Comment thread CIO/CMinimapWindow.cpp
Comment thread Texture.cpp Outdated
return;

const Extent tileSize = getSize();
if(static_cast<int>(tileSize.x) <= 0 || static_cast<int>(tileSize.y) <= 0)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(half-)invalid check, check for zero only

Comment thread Texture.cpp Outdated
if(static_cast<int>(tileSize.x) <= 0 || static_cast<int>(tileSize.y) <= 0)
return;

glColor4f(1, 1, 1, 1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

color does need to be reset in some places. But can be moved to those specific places rather than reset globally.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes, OGl calls are costly. So don't do them if not necessary, but only where you actually change things

@morganchristiansson morganchristiansson Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's very cheap compared to copying 33MB surface from system RAM 60 times per second. 2GB/s.

Also gl commands are queued until glSwapBuffers()
(I didn't know this, just asked my AI)

I'm actually adding more gl calls in gl2-terrain #59 to improve reviewability of diff, reverting to one DrawTriangle() at a time. Essential for reviewability. And actually fixed a bug when reverting to make it closer to original.
I think it's fine but can improve it post-merge.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sure, but then driving your car around the block for no reason is also cheap compared to flying overseas ;-)
Just want to avoid "premature pessimization" by adding unnecessary things that take more cycles than a couple additions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yep. I fixed it's good call. OCD matters for code.

Comment thread Texture.h
#include <Point.h>
#include <SDL.h>

/// Wraps a texture with RAII and provides draw methods.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

40407bd5d0a385d3e6b6fbac531a352b33b7cb0b07809f9213765ac09c71df6c removed the docstrings.
Those were relevant

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I thought it was mostly redundant. This comment doesn't really add anything?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Docstrings do help as they get rendered in IDEs at usage site.
The class one could mention it wraps an "OpenGL texture (handle)" as "texture" is quite generic: All the bmps are textures

And e.g. for the load method:

    /// Load from a 32-bit or 8-bit paletted SDL surface.
    /// For 8-bit surfaces, optional colorkey is respected (keyed pixels become transparent).

That is more than the the signature provides. So a short sentence per method in the header is useful
Especially e.g.:

    /// Draw the texture stretched to fill the given rect.
    void draw(const Rect& destRect) const;

How else would you know it is stretched?

@morganchristiansson morganchristiansson Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I mean Texture class name is self-explanatory.
draw with target size is implied stretched.
it was called drawStretched in previous PR but you wanted it just draw which is fine.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I mean Texture class name is self-explanatory.

As I said: It could mean a graphics file or a hardware texture. So knowing this is an "OpenGL texture handle" at the cost of 1 line is fine.

My point about comments/docstrings is whether they save time.
Bad:

/// Get color
unsigned getColor();

// Make ARGB: alpha<<24 | red <<16 [...]
color = a << 24 | r << 16 | g << 8 | b

But the short docstrings quoted above do add information/context:

  • OpenGL texture handle -> ready to use with OpenGL
  • support for 8 & 32 bit with transparency
  • stretching semantics

So for docstrings in headers I'd lean to including them (but keep them succinct) unless they state literally the same as the method name, like getColor above, but even then: "Get color as ARGB value" would add value.

See the borderBeginSize case: In 4yrs time you won't remember what you meant even when it's obvious now ;-)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok fair. I've not used docstrings in c++ before and AI started adding them unprompted and I thought it was more slop comments.

Comment thread CIO/CMinimapWindow.cpp Outdated
const int arrowIdx = MAPPIC_ARROWCROSS_ORANGE;
const auto& dispRect = map->getDisplayRect();
const Position arrowCenter =
dispRect.getOrigin() + Position(dispRect.getSize().x / 2, dispRect.getSize().y / 2);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
dispRect.getOrigin() + Position(dispRect.getSize().x / 2, dispRect.getSize().y / 2);
dispRect.getOrigin() + dispRect.getSize() / 2u;

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