imgui icon indicating copy to clipboard operation
imgui copied to clipboard

New Polyline rendering

Open thedmd opened this issue 1 year ago • 8 comments

This is follow up to #2964 Render thick lines with correct thickness and beveled corners PR.

image

New implementation introduce more features, fixes some long standing issues and aim to be fast.

Code is feature complete, no todo's are left other than bugfixes.

Change is rather large and may look intimidating. Please read description, comment, criticque code and suggest improvements.

New Features

Fully Variable Thickness

New implementation support full range of thickness for Anti-Aliased and Aliased paths.

https://github.com/user-attachments/assets/bf661525-d934-4d13-b240-5eccb707489f

Support for ImDrawListFlags_AllowVtxOffset

With 16-bit indices single command can handle up to 65536 vertices. Huge polylines are now split across multiple draw commands if necessary.

Cap Types

Support for caps on open polylines.

ImDrawFlags_CapNone ImDrawFlags_CapButt
(new default)
ImDrawFlags_CapSquare ImDrawFlags_CapRound
AA
Mesh
image image image image
AA
1:1
image image image image
Aliased
Mesh
image image image image
Aliased
1:1
image image image image

Important notes:

  • ImDrawFlags_CapButt is new default
  • ImDrawFlags_CapNone match old behavior which leave anti-aliased polyline without a cap
  • ImDrawFlags_CapButt is new default, this make all edges on checkbox anti-aliased image
  • ImDrawFlags_CapRound is pretty yet expensive in relation to other caps types
  • ImDrawFlags_CapRound use adaptive rendering via PathArcTo

Join Types

Support for more join types.

Edit: Rounded joins (and caps) are not more refined, please see https://github.com/ocornut/imgui/pull/7972#issuecomment-2345018733 below.

ImDrawFlags_JoinMiter
(default)
ImDrawFlags_JoinMiterClip ImDrawFlags_JoinBevel ImDrawFlags_JoinRound
AA
Mesh
image image image image
AA
1:1
image image image image
Aliased
Mesh
image image image image
Aliased
1:1
image image image image

Important notes:

  • ImDrawFlags_JoinMiter collapse to ImDrawFlags_JoinBevel when miter limit is exceeded (see below)
  • ImDrawFlags_JoinMiterClip provide smooth transition between Miter and Bevel, it is bit more expensive
  • ImDrawFlags_JoinBevel is require bit more math for Anti-Aliased polyline
  • ImDrawFlags_JoinRound is pretty yet expensive in relation to other join types
  • ImDrawFlags_JoinRound use adaptive rendering via PathArcTo

Miter Limit

Miter distance count from the control point to very tip of the triangle forming a join. Sharpness of the join can be limited by using 'Miter Limit'. Miter does collapse to Bevel, MiterClip does smooth transition to Bevel.

Miter limit match SVG2 stroke-miterlimit behavior.

image (borrowed from w3.org)

AddPolyline gained new parameter miter_limit:

IMGUI_API void  AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness, float miter_limit = -1.0f);

Default Miter Limit is set 4.0 in ImDrawListSharedData (internal API) and not exposed to the user.

Fallback

Previous implementation of polyline is still available under different name:

IMGUI_API void  AddPolylineLegacy(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness);

New implementation does change behavior. ImDrawListFlags_LegacyPolyline flag is a gateway to opt-in to old rendering.

New fancy tools are new and fancy, but also not mature. Setting ImDrawListFlags_LegacyPolyline on ImDrawList will cause PathStroke() to route all rendering to old AddPolyline(). This in turn for all practicall purposes will route rendering of all primitives to old code path.

Reasons to set ImDrawListFlags_LegacyPolyline flag:

  • When you see noticable dips in performance. New implementation does not use texture based rendering, which make AddPolylineLegacy faster and generate less geometry.
  • When new behavior cause issues in rendering. AddPolylineLegacy does not support caps and can generate wrong geometry on acute angles.

Related issues

https://github.com/ocornut/imgui/issues/2183 Bug in drawing thick antialiased polylines

Status: Fixed ✅

Old New
image image

https://github.com/ocornut/imgui/issues/3366 Polyline with sharp angles causes segments to nearly disappear

Status: Fixed ✅

Old New (default) New (large Miter Limit)
image image image

https://github.com/ocornut/imgui/pull/4091 Optimize IM_NORMALIZE2F_OVER_ZERO

This PR does add IM_NORMALIZE2F_OVER_ZERO_PRECISE() that is built on top of SSE2 _mm_rsqrt intrinsics. SSE2 states that this function is an approximation of 1.0f / sqrt(x) and does have relatively large error. New macro does use new ImRsqrtPrecise() instead of ImRsqrt.

ImRsqrtPrecise() improve precission by converging on answer by performing single step Newton-Raphson method. Exackly like in famous rsqrt found in Quake source code. This single step is enough to keep geometry consistent and to not produce cosines larger than 1.

On debug (and non SSE2) builds ImRsqrtPrecise fallback to 1.0f / sqrt(x), because this is faster than to perform NR step in unoptimized code. On release SSE2 builds this does use method described above.

Performance

Early measurements compare old AddPolyline() with and without texture polylines against new implementation. I was unable to quickly test #2964, because it crashed on some tests.

Observations:

  • consecutive calls (hot path) does have differen performance characteristics than first call (cold path)
  • new implementation place itself in between old AddPolyline() and #2964
  • observation is based on test made while in development
  • ~~Test performance against #2964~~ as adviced, comparison with previous PR is not necessary

Please draw your own conclusions, or beter do your own benchmark to see change in real world.

Debug Release
image image

Note:

  • Old AddPolyline() does sometimes produce broken meshes in some tests due to lack of ImDrawListFlags_AllowVtxOffset support

Implementation notes

General:

  • Implementation see rather heavey use of macros, all are names like IM_POLYLINE_xxx
  • They should improve readability and see code flow
  • IM_POLYLINE_TRIANGLE_xxx are overly verbose to experiment with different methods of filling index buffer
  • Will be simplified to drop unused arguments after we settle on one particular implementation
  • Uses as little local variables as possible
  • accessing them on Debug builds does generate extra mov instructions
  • computations are done as expressions with intent to end with single assignment to final location in the buffer
  • all _PolylineXXX are basically for loops with a bit of math at the beginning and 'switch' routing to selected join geometry generation code
  • core responsible for generating geometry is commented with 'ascii art' explaining where magic values in indices came from
  • code paths are annotated with IM_LIKELY and IM_UNLIKELY which are resolved to [[likely]] and [[unlikely]] when available
  • this does guide optimizer, but does not affect Debug builds

New implementation does split problem to three cases:

  • _PolylineThinAntiAliased - for thickness <= 1, technically less than AA fringe
  • skips generating solid core geometry
  • skips MiterClip logic all together (fringe is 1 pixel thick so jump from Miter to Bevel will not be noticable in most cases)
  • _PolylineThickAntiAliased - for thickness > 1
  • presence of AA fringe bump complexity of generated geometry, in most extreme cases can emit 17 vertices per join, it is rare
  • _PolylineAliased - used for any thickness, does not have AA fringe

AddPolyline():

  • act as dispatcher calling one of the methods above
  • is responsible of computing polyline normals / segment lengths used by all subroutines
  • With SSE2 computes 4 normals at once, then 2 normals at once and finally fall back to scalar implementation
  • is responsible for sanatization of input values, _PolylineXXX does not need to validate input

Rounted caps and joins:

  • _PolylineEmitArc and _PolylineEmitArcsWithFringe post-processing step responsible for generating adaptive arcs
  • ImAtan2 and Acos are use for every join to feed PathArcTo
  • ImAtan2 is used for caps PathArcTo
  • Implementation definitely can be improved in the future
  • All queued arcs are processed at the end of _PolylineXXX call
  • Arcs are queued in ImDrawList::_Path, they're appended at the end
  • [x] Change format so place for encoded arcs can be pre-allocated to avoid memory corruption

thedmd avatar Sep 11 '24 09:09 thedmd