Skip to content

Replace legacy OpenGL implementation with Silk.NET-based renderer - #339

Open
dsn27 wants to merge 10 commits into
masterfrom
claude/opengl-silk-net-migration-6pk28g
Open

Replace legacy OpenGL implementation with Silk.NET-based renderer#339
dsn27 wants to merge 10 commits into
masterfrom
claude/opengl-silk-net-migration-6pk28g

Conversation

@dsn27

@dsn27 dsn27 commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR replaces the legacy Windows Forms OpenGL implementation with a modern Silk.NET-based renderer (PaintToSilkGL). The change modernizes the 3D rendering pipeline while maintaining API compatibility through the existing IPaintTo3D interface.

Key Changes

  • Removed legacy OpenGL code:

    • CADability/OpenGL.cs (51,941 lines) - Legacy OpenGL bindings
    • CADability.Forms/PaintToOpenGL.cs (2,278 lines) - Old Windows Forms OpenGL renderer
    • CADability/OpenGlList.cs (140 lines) - Legacy display list implementation
  • Added Silk.NET-based implementation:

    • CADability.Forms/PaintToSilkGL.cs (1,040 lines) - New modern OpenGL renderer using Silk.NET
    • CADability.Forms/WglContext.cs (127 lines) - WGL context management for Windows
    • CADability.Forms/OpenGLList.cs (98 lines) - Display list implementation for Silk.NET
    • GLSL shader files for surface and edge rendering:
      • surface.vert / surface.frag - Phong lighting for surfaces
      • edge.vert / edge.frag - Simple edge rendering
  • Updated integration points:

    • CADability.Forms/CadCanvas.cs - Updated type check from PaintToOpenGL to PaintToSilkGL
    • CADability.Forms/CadFrame.cs - Updated factory to instantiate PaintToSilkGL
    • CADability/PaintToOpenGl.cs - Added IPaintTo3DFlatText interface extension
    • CADability.Forms/CADability.Forms.csproj - Enabled unsafe code blocks
    • tests/CADability.Tests/ProjectTest.cs - Updated test references
  • Simplified GDI bindings:

    • CADability/Gdi.cs - Reduced from 609 to 24 lines, keeping only essential P/Invoke declarations

Implementation Details

  • The new PaintToSilkGL class implements both IPaintTo3D and IPaintTo3DFlatText interfaces, maintaining backward compatibility
  • Supports off-screen rendering via framebuffer objects for bitmap output
  • Uses modern GLSL shaders (version 330 core) for lighting and rendering
  • Maintains the same public API surface, allowing existing code to work without changes

https://claude.ai/code/session_014HjM12HeHU7a7n9DJx91Y4

claude added 2 commits July 22, 2026 09:32
Replace the OpenGL 1.x fixed-function pipeline with a modern,
shader-based renderer built on Silk.NET.OpenGL. The rendering
implementation moves out of the netstandard2.0 core (CADability) and
into the WinForms host (CADability.Forms), which owns the platform GL
context.

Core (CADability, netstandard2.0):
- Remove OpenGL.cs (the ~52k-line Gl/Glu/Wgl/User/Kernel P/Invoke
  bindings) and OpenGlList.cs; the core no longer talks to OpenGL.
- Trim Gdi.cs to the small GDI32 stub still needed for kerning-pair
  queries (CreateCompatibleDC/DeleteDC/SelectObject/DeleteObject/
  GetKerningPairs).
- Add the IPaintTo3DFlatText interface so text glyphs can be drawn
  unlit (in their ColorDef color) instead of being shaded like a 3D
  surface.
- Text.cs: measure glyph advance widths via GDI+ MeasureString
  (GenericTypographic) instead of the removed GetCharABCWidths
  P/Invoke, and drive FlatTextMode while tesselating glyphs.

Forms (CADability.Forms, net8.0-windows):
- Add PaintToSilkGL, the IPaintTo3D renderer that replaces
  PaintToOpenGL.
- Add WglContext for WGL context creation and Silk.NET GL binding
  (with the opengl32.dll fallback for core 1.x entry points).
- Add PaintToSilkGLList (OpenGLList.cs), a VAO/VBO-based replacement
  for fixed-function display lists (interleaved position+normal for
  surfaces, position for edges, sub-lists for text).
- Add GLSL 3.30 core shaders (surface + edge) with two-sided lighting
  and an unlit path for flat text.
- Reference Silk.NET.OpenGL 2.* and embed the shaders as resources.
- Wire CadCanvas/CadFrame to PaintToSilkGL.

Known deferred work (marked in code):
- Off-screen bitmap rendering (PaintToBitmap / CreatePaintInterface)
  is not yet implemented and throws NotImplementedException.
- Line dash patterns and icon/bitmap display are stubbed no-ops.

Target frameworks are unchanged: CADability stays netstandard2.0 and
the Forms/App/Tests projects stay net8.0-windows. No netDXF or
ACadSharp changes are included.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HjM12HeHU7a7n9DJx91Y4
The old PaintToOpenGL.PaintToBitmap rendered into a GDI bitmap DC using
PFD_DRAW_TO_BITMAP, which only works with Microsoft's software OpenGL
1.1 renderer — exactly the fixed-function pipeline the Silk.NET
migration removed. Modern hardware-accelerated core contexts refuse
bitmap DCs, so this was left throwing NotImplementedException.

Replace it with a framebuffer-object path:

- Init(Bitmap): creates a hidden NativeWindow to host the WGL context
  (never shown), compiles the shared shaders, and renders into an FBO
  (RGBA8 color + Depth24Stencil8 renderbuffers) of the bitmap's size.
- FinishPaint(): in off-screen mode reads the FBO back into the target
  bitmap (BGRA ReadPixels into LockBits, then a vertical flip because
  GL rows are bottom-up) instead of swapping buffers.
- Dispose(): performs a best-effort readback first, because the
  printing path (LayoutView) disposes the paint interface without ever
  calling FinishPaint, then deletes the FBO resources and destroys the
  hidden window.
- MakeCurrent(): rebinds the FBO in off-screen mode.
- IsBitmap now reports the real off-screen flag, so core code keeps
  avoiding display lists when painting to a bitmap, as it did with the
  old renderer.
- PaintToSilkGL.PaintToBitmap: full port of the old projection/extent
  setup, no longer delegating to the throwing OpenGlCustomize stub.
- CadFrame.CreatePaintInterface: restored (used by LayoutView
  printing); no longer throws.

The CADability core project is unchanged (stays netstandard2.0);
CADability.Forms still targets net8.0-windows.

Verified by compiling CADability, CADability.Forms, CADability.App and
CADability.Tests with the .NET 8 SDK (EnableWindowsTargeting on Linux),
0 errors. Runtime behavior needs a Windows machine with an OpenGL 3.3
capable driver; note the old reference bitmaps in the tests were
produced by the GL 1.1 software rasterizer and may need regeneration
for the shader-based renderer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HjM12HeHU7a7n9DJx91Y4
@dsn27
dsn27 changed the base branch from claude/opengl-silk-net-migration-humming-snowflake to master July 22, 2026 10:47
claude added 8 commits July 22, 2026 11:34
…lkGL

The coordinate cross (ModelView.PaintCoordCross, also GDI2DView) draws
its x/y/z axis labels by calling IPaintTo3D.Text directly instead of
going through a Text GeoObject. PaintToSilkGL.Text was an empty stub —
its comment assumed all text arrives pre-triangulated via Triangle(),
which is only true for Text GeoObjects honoring TriangulateText — so
the axis labels silently disappeared. (In a top view the z label is
additionally skipped by the SameDirection checks, which is why exactly
"x" and "y" were reported missing.)

Implement Text() by routing the call through a transient Text
GeoObject: PrePaintTo3D creates the per-character display lists in the
font cache (glyphs tessellated at em size 1, rendered unlit via
FlatTextMode so labels keep the exact info color), PaintTo3D replays
them with the placement, alignment and kerning of the em-based recipe
that the old wglUseFontOutlines implementation also used. The
transient call forces TriangulateText=true, because with
TriangulateText=false Text.PaintTo3D would call back into
IPaintTo3D.Text and recurse.

Verified by compiling CADability and CADability.Forms (0 errors);
visual verification needs a Windows machine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HjM12HeHU7a7n9DJx91Y4
The coordinate cross is painted right after ProjectedModel.Paint, which
ends with PaintFaces(CurvesOnly) — so paintSurfaces is false while the
background is drawn (ModelView only restores PaintFaces(All) after
PaintBackground). The glyphs of the x/y/z labels replay through List(),
which skips DrawListSurface when paintSurfaces is false, so the labels
stayed invisible while the axis lines (Polyline) and arrows (Triangle)
drew fine, as immediate-mode calls are not gated.

The old renderer's IPaintTo3D.Text called glCallList directly without
any paint-mode gate, so direct text always drew. Restore that behavior
by forcing paintSurfaces/paintEdges on for the duration of the
transient Text paint and restoring them afterwards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HjM12HeHU7a7n9DJx91Y4
…rites

Inserted bitmaps (Picture GeoObjects) were invisible because
RectangularBitmap and the whole Prepare/Display bitmap family were
no-op stubs in PaintToSilkGL.

Add a texture pipeline replacing the old fixed-function texture and
raster operations:

- texture.vert/.frag: textured draw with u_color tint and discard at
  alpha <= 0.5, matching the old glAlphaFunc(GL_GREATER, 0.5).
- PrepareBitmap(Bitmap): uploads the bitmap as an RGBA8 texture
  (NEAREST filter, rows flipped to GL orientation), cached per Bitmap.
- RectangularBitmap: textured world-space quad with the same corner/
  texcoord layout as the old GL_QUADS path; records into the display
  list (new TexQuad entries in PaintToSilkGLList) or draws immediately.
- PrepareBitmap(Bitmap,x,y) + DisplayBitmap: screen-aligned unscaled
  sprite with pixel anchor, replacing glRasterPos/glDrawPixels (Icon
  GeoObjects).
- PrepareIcon + DisplayIcon: white alpha-mask texture tinted with the
  current color, centered on the point, replacing glBitmap (hotspot
  markers).
- List() replays TexQuads/Sprites in the faces phase only, so the
  two-phase (faces+curves) replay of categorized display lists draws
  them once.
- Dispose deletes the texture program and all cached textures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HjM12HeHU7a7n9DJx91Y4
SetLinePattern and PreparePointSymbol were the last visible no-op stubs
in PaintToSilkGL: all line styles rendered solid and Point objects drew
as plain dots.

Dashed lines (glLineStipple replacement):
- Edge vertices carry a second attribute: the cumulative distance along
  the polyline in world units (edge stride 3 -> 4 floats), recorded at
  tessellation time, so dash phase continues across segments instead of
  restarting at every tessellation segment.
- The edge fragment shader converts that distance to screen pixels
  (u_distScale from PixelToWorld) and discards fragments in the gap
  segments of the pattern (u_pattern[8], noperspective interpolation).
- SetLinePattern normalizes patterns like the old stipple code: only
  the proportions are used, repeating over 16 screen pixels with at
  least 1 px per segment; null/empty/degenerate patterns mean solid.
- Display list edge batches now carry (color, pattern) so per-entity
  patterns replay correctly; 2D overlay lines stay solid.

Point symbols (glBitmap replacement):
- Port the BitmapList point symbol strip loader (PointSymbols.bmp /
  PointSymbolsB.bmp via BitmapTable + ImageList, select square at 12).
- Points() maps symbol flags to the same bitmaps as the old renderer
  (base symbol & 0x07, Circle/Square modifiers, Select overrides all,
  bold offset 6 when UseLineWidth) and draws them as screen-aligned
  sprites via DisplayIcon in the current color, which also records
  correctly into display lists. A pure Dot symbol now uses the dot
  bitmap instead of a 1 px GL_POINTS vertex, making it list-safe.
- PreparePointSymbol pre-uploads the mask textures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HjM12HeHU7a7n9DJx91Y4
…feedback

1. Points/dimensions invisible (deselected dimension: text vanished,
   arrows hollow; drawn points not visible): List() gated surface
   content on paintSurfaces and sprites on the faces phase. But the
   old glCallList replayed the entire list unconditionally — the
   PaintFaces mode controls what core code emits while recording (and
   the polygon offset), not what an already recorded list draws.
   Curve-phase lists legitimately contain surface geometry (dimension
   text, filled arrow heads, point symbol sprites). Replay everything
   unconditionally.

2. Massive memory consumption (e.g. after adding a sphere): dropped
   display lists never freed their GPU buffers — the old OpenGlList
   finalizer/FreeLists pattern was lost in the migration, and nothing
   calls IPaintTo3DList.Dispose. The unscaled lists are rebuilt on
   every zoom change and construction feedback creates lists per
   frame, so VRAM grew without bound. PaintToSilkGLList now queues its
   VAO/VBO ids in a finalizer; FreeDeleted deletes them on the render
   thread from OpenList and FreeUnusedLists. Also release the CPU-side
   vertex arrays after GPU upload — they were kept alive forever,
   roughly doubling every display list's memory.

3. Split view black (second view showed nothing): every canvas created
   its own WGL context, but the categorized display lists live on the
   Model and are painted by all views — VAOs from one context are
   invalid names in another. All views (and the off-screen FBO path)
   now share one process-wide WGL context, made current per canvas
   against that canvas' DC (replacement for the old MainRenderContext
   + wglShareLists). Dispose no longer deletes the context or the
   shared GL api object, only per-instance resources and the DC.

4. Sphere shading "unsmooth": the specular term used the direction
   toward the world origin (-vFragPos) as the view vector, smearing
   the highlight into irregular bands across curved surfaces. Use the
   constant per-view eye direction (orthographic viewer at infinity,
   like the old fixed-function pipeline) and the old broad shininess
   (5 instead of 32, at half strength since per-fragment specular is
   punchier than Gouraud).

5. Trim feedback grey instead of yellow: ActionFeedBack calls
   SelectedList(list, -1), but the wobble loop never runs for radius
   <= 0 and the center pass forced the original colors. Implement the
   old radius <= 0 semantics: draw the whole list once in the select
   color, translated 2*precision toward the viewer so the highlight
   wins the depth test against the object itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HjM12HeHU7a7n9DJx91Y4
The zoom-in memory explosion with curved solids (70 MB -> 1.5 GB with a
sphere) had a second ingredient besides the missing finalizer: after
UploadToGpu nulls the CPU arrays, a dead display list is a tiny managed
object holding megabytes of invisible driver memory — the GC sees no
reason to ever collect, so the finalizers that queue the GPU buffers
for deletion never run. Display lists are rebuilt on every zoom step
(re-triangulation at finer precision, unscaled lists), so driver memory
grew without bound. 2D objects produce tiny lists, which is why they
did not show the effect.

- UploadToGpu registers the buffer size via GC.AddMemoryPressure;
  Dispose and the finalizer remove it, so the GC collects dropped
  lists roughly as if their geometry still lived on the managed heap.
- MakeCurrent (start of every frame) now also processes the pending
  deletion queue, so reclaim does not wait for the next OpenList.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HjM12HeHU7a7n9DJx91Y4
…e phase

Dimensions had no PaintTo3DList override, so the base implementation
registered them for the curve category only. Curve display lists are
recorded under PaintFaces(CurvesOnly), i.e. PaintSurfaces == false —
and a dimension's text glyphs and filled arrow heads (solid Hatch) are
Face geometry whose PaintTo3D skips emission in that mode. The text
and arrow fillings therefore never made it INTO the recorded list;
fixing the replay side could not help. The old renderer got away with
it because its default text path (TriangulateText == false,
wglUseFontOutlines) bypassed Face.PaintTo3D entirely.

- Dimension now registers for both categories (lists.Add(layer, true,
  true, this)) like solids do: the face phase records the text and
  arrow faces, the curve phase records the dimension/extension lines.
- PaintToSilkGL.PaintSurfaces additionally reports true while
  FlatTextMode is active, so glyph display lists created lazily during
  curve-phase recording are never cached as empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HjM12HeHU7a7n9DJx91Y4
Big faces add millions of floats per call; growing the List<float> by
repeated doubling roughly doubles the transient allocations. Reserve
the needed capacity in one step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HjM12HeHU7a7n9DJx91Y4
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