Duxel - Dear ImGui 스타일의 고속 GUI 라이브러리 - slog

오래간만의 슬로그네요.

Duxel은 앞전에 MewUI의 동작성에 자극을 받아 평소에 만들고 싶었던 Dear ImGui 스타일의 GUI 라이브러리입니다. 관련된 정보는 아래의 링크를 통해 확인할 수 있어요.

100% 바이브 코딩으로 진행하고 있으며 GPT-5.2-Codex → GPT-5.3-Codex 모델로 GitHub Copilot을 이용해 만들고 있습니다.

슬로그를 통해 달성하고 싶은 계획은 다음과 같아요.

  • 좀 더 사용하기 편한 구조로 변환 (취소)
    Duxel은 별도의 렌더 트리가 없어요. 그래서 WinForms나 WPF에 익숙한 분께는 거칠어요. 그냥 OnRender 메소드에서 일일이 화면에 그려서 구성하는 방법이기 때문이에요.
    그래서 Fluent API를 이용해서 선언적인 구조로 변경하려고 했습니다만…
    꽤 긴 삽질을 한 이후에 이런 디자인이 ImGui 스타일과 맞지 않다는 결론에 도달했습니다. 그래서 취소
    → 이것보다 DSL 문법을 적극 활용하는 방법으로 고도화 하려고 합니다.

  • 모든 위젯의 동작성 확보
    대부분의 위젯이 잘 동작하지만 위치가 이상하거나 오동작 하는 위젯이 상당히 있습니다. 이를 보정하는 작업입니다.

  • 깔끔한 글자 출력 및 속도 보장
    Vulkan을 직접 다루다 보니 DirectWrite 수준의 글자를 출력하기가 정말로 어렵더라고요… 관련 전문 지식도 없었던지라 엄청난 삽질 이후 DirectWrite를 추가하게 되어 구조적인 문제가 현제는 있습니다. 관련해서 Chromium 수준의 폰트 렌더링을 확보하는 것입니다.

  • 그 다음은 크로스플랫폼
    Duxel은 원래 크로스플랫폼 용 라이브러리를 위해 시작하였어요. 그런데 아직은 Windows에서만 동작합니다. 리눅스 및 iOS에서 동작하도록 확장할 예정이에요.

  • Silk.NET의 의존성 제거
    Vulkan의 기능을 이용하기 위해 Silk.NET을 썼지만 알기로는 NativeAOT와 친화적이라고 알고 있었는데 Trim이 안되는 사소한 문제가 있어서 최종 실행 파일 크기가 4MB를 넘는 문제가 있습니다. Silk.NET를 걷어내서 3M 미만으로 실행파일을 줄이는 것이 목표입니다.

결국엔 뭘 하고 싶은거지?

MewUI가 Window Forms 및 WPF의 굴레에서 벗어나 크로스플랫폼으로 NativeAOT의 장점들을 활용할 수 있게 된 것 처럼

Duexl은 고속/실시간 2D 렌더링이 필요한 다양한 목적에 활용할 수 있도록 발전할 예정입니다.
그런데 바이브코딩을 해도 상당한 시간과 노력이 들어가는 것은 당황스럽네요 ^^;

8개의 좋아요

먼저 브랜딩 부터…

[ Duxel ] — Immediate GUI for .NET

^^;

4개의 좋아요

0.1.15-preview

주요 기능 추가

  • 플랫폼 텍스트 백엔드 추상화: IPlatformTextBackend 인터페이스로 아틀라스 파이프라인과 분리된 크로스 플랫폼 텍스트 래스터라이제이션 지원.
  • DWrite 백엔드 추가: WindowsPlatformTextBackend가 텍스트-런 단위로 COM 호출 → 글리프별 호출 대비 성능 개선.
  • 폰트 크기 제어 API: SetDirectTextBaseFontSize로 행 높이와 독립적으로 DWrite 기본 em 크기 설정 가능.

주요 개선 사항

  • 위젯 텍스트 렌더링 전면 DWrite 경로로 마이그레이션 (Button, Tab, Menu 등 전체).
  • 텍스트-런 API 도입: 글리프별 대신 런 단위 COM 호출 → 오버헤드 감소.
  • 캐싱 최적화: TryMeasureDirectText 결과를 재사용해 이중 래스터라이제이션 제거.
  • 폰트 아틀라스 진단/캐시 옵션 추가: 환경변수로 디스크 캐시 토글, 빌드 추적, 텍스처 덤프 가능.
  • Vulkan 폰트 명령 진단 강화: 명령 추적 및 바운드 검증 환경변수 제공.
  • 캐시 무효화 로직 개선: UiFontResourceCodepointSignature 추가, 프레임별 코드포인트 스냅숏 도입.

주요 버그 수정

  • 텍스처 ID 충돌 해결: 동적 아틀라스와 DWrite 텍스트 ID 범위 분리.
  • 한글 미출력 문제 해결: 공백 런 처리 개선으로 공백 전용 런 알파 문제 제거.
  • 폰트 크기 불일치 수정: LineHeight 대신 정확한 em 크기 사용.
  • 수직 중앙 정렬 문제 해결: Y 오프셋 추가로 행 높이 내 중앙 배치.
  • Vulkan 스테이징 버퍼 레이스 수정: 펜스 대기 후 메모리 쓰기 순서 보장.
  • 스왑체인 재생성 오류 처리 개선: 실패 시 안전하게 false 반환.
  • 버퍼 크기 검증 강화: 포맷별 정확한 바이트 계산 및 부족 시 패딩 처리.

기존 폰트 아틀라스를 모두 걷어내고 이제 DWrite의 글리프 방식으로 대체하였습니다.

curl -sL https://raw.githubusercontent.com/dimohy/Duxel/refs/heads/main/samples/fba/font_style_validation_fba.cs -o - | dotnet run -

1개의 좋아요

0.2.0-preview

  • 주요 기능 추가
    • 쇼케이스/샘플 범위 확장 — 종합 워크스페이스로 확장 구현
    • 확장 포인트 문서화 및 샘플 보강 — 고급 UI 조합 경로 확인 가능하도록 구현
  • 주요 개선 사항
    • 쇼케이스 배치와 레이아웃 — 배치 안정화 정리
    • 내장 데모 창 동작 — 위치/크기 및 일관성 정리
    • 릴리스 문서 — 링크 및 메타데이터 동기화 정리
  • 주요 버그 수정
    • 멀티라인 입력과 마크다운 줄바꿈 — carriage return 제거 수정
    • 한글 IME 입력 지연 — 즉시 반영 및 끊김 제거 수정
    • 쇼케이스 레이아웃 회귀 — clipping/겹침/폭 혼선 수정
    • 라이브러리 top-most 동작 — 창 스택 순서 안정화 구현
curl -sL https://raw.githubusercontent.com/dimohy/Duxel/refs/heads/main/samples/fba/all_features.cs -o - | dotnet run -

Duxel에서 제공하는 기능들을 전체 점검하였고 이를 확인할 수 있는 all_features.cs를 개선하였습니다.

다음으로 Skil.NET 의존성을 제거하고 DSL 사용 샘플을 다양하게 추가하면서 Duxel의 가능성을 좀 더 점검할 예정입니다.

※ Markdown Preview 기능을 추가했습니다. 커스텀 위젯을 만들 수 있게 하였고 그 첫번째 위젯입니다.

all_features.cs를 통해 확인할 수 있어요! ^^

5개의 좋아요

0.2.1-preview

주요 기능 추가

  • [기능] 다중 윈도우 지원 (모달/모들리스) — ShowModal()로 소유자 윈도우 비활성화 차단형 대화상자, ShowModalAsync()로 비동기 모달, ShowModeless()로 독립적인 비차단 윈도우를 각각의 DuxelAppSession 수명주기로 구동.
  • [기능] 시스템 트레이 아이콘 지원 — WindowsTrayIconHost로 트레이 아이콘, 툴팁, 컨텍스트 메뉴, 더블클릭 핸들러, 최소화 시 트레이 숨기기, 닫기 시 숨기기를 Win32 Shell API로 구현.
  • [기능] 순수 Vulkan P/Invoke 바인딩 레이어 — Silk.NET Vulkan을 LibraryImport 기반 직접 바인딩(VulkanApi, VulkanStructs, VulkanEnums, VulkanHandles, VulkanExtensions, VulkanMarshaling)으로 대체, NativeAOT 완전 호환.
  • [기능] ClearType 서브픽셀 텍스트 렌더링 셰이더 — DirectWrite ClearType 품질을 위한 RGB 채널별 coverage 출력 프래그먼트 셰이더(imgui_subpixel.frag) 추가.
  • [기능] Windows WIC 이미지 코덱 — System.Drawing.Common을 순수 COM 기반 Windows Imaging Component 디코더로 대체, GIF 애니메이션 프레임 compositing과 알파 블렌딩 지원.

주요 개선 사항

  • [개선] 세션 기반 앱 수명주기 — DuxelApp.RunCore()에서 DuxelAppSession 분리, 독립적 세션 인스턴스의 이중 스레드 렌더 루프, idle frame skip, 증분 폰트 아틀라스 스케줄링 지원.
  • [개선] 윈도우 옵션 확장 — MinWidth/MinHeight, Resizable, 최소화/최대화 버튼 표시, CenterOnScreen/CenterOnOwner, 소유자 윈도우 핸들, 커스텀 아이콘(파일/메모리), WindowCreated 콜백 추가.
  • [개선] 플랫폼별 진입점 전환 — FBA 샘플이 DuxelApp.Run() 대신 DuxelWindowsApp.Run()Duxel.$(platform).App 패키지 지시문 사용.

주요 버그 수정

  • [버그] NativeAOT 게시를 방해하던 System.Drawing.Common 의존성 제거.

Packaging / Release

  • Silk.NET Vulkan 및 System.Drawing.Common 패키지 의존성 제거.
  • 실험적 레이어 텍스처 캐시 백엔드(UiLayerCacheBackend.Texture) 아카이브 처리.
  • 내장 데모 윈도우(UiImmediateContext.DemoWindows.cs) 제거.
3개의 좋아요

0.2.2-preview

주요 기능 추가

  • [기능] DSL 스크린 런타임 (UiDslScreen) — 핫리로드(managed)와 소스 생성(NativeAOT) 통합 DSL 렌더링, .duxel-theme 핫리로드, RequestTheme() API, 이벤트/값 바인딩 통합.
  • [기능] DSL 이벤트/값 바인딩 — UiDslEventBinder(fluent per-id 버튼/체크박스 콜백)와 UiDslValueBinder(fluent IUiDslValueSource 래퍼)로 인터페이스 수동 구현 제거.
  • [기능] DSL 제어 흐름 — If/ElseIf/Else, Visible, ForEach(범위 기반 템플릿 확장), Switch/Case/Default, Set, Text Bind="key" 데이터 바인딩.
  • [기능] 테마 프리셋 10종 — ImGuiDark, ImGuiLight, ImGuiClassic, Nord, SolarizedDark, SolarizedLight, Dracula, Monokai, CatppuccinMocha, GitHubDark 즉시 전환 지원.
  • [기능] ThemeDemo 샘플 — DSL 기반 테마 핫리로드 데모 앱, 테마 선택 Combo, 제어 흐름 데모, NativeAOT 지원.
  • [기능] 테마 시스템 전면 개편 — UiTheme struct를 110 색 InlineArray로 재설계, 위젯별 색상 토큰(UiStyleColor enum), InitWidgetDefaults() 기본값 캐스케이드, .duxel-theme 파일 파서의 베이스 프리셋 상속.

주요 개선 사항

  • [개선] 위젯 테마 세분화 — 기존 ~35개 글로벌 색상에서 110개 위젯별 토큰으로 확장, Button/Checkbox/RadioButton/Input/Slider/Drag/Combo/Selectable/MenuItem/Tab/TreeNode/Table/Tooltip/ListBox/ProgressBar/Separator의 border·state 개별 지정.
  • [개선] 위젯 border 렌더링 — Button, Checkbox, RadioButton, Input, Slider, Drag, Combo, ListBox, ProgressBar 위젯이 hover/active 상태별 독립 border 색상 렌더링.
  • [개선] 텍스트/불릿 수직 중앙 정렬 — glyph metrics 기반 시각 중심 계산으로 기존 heuristic ascent offset 대체.
  • [개선] Combo 위젯 이벤트 알림 — 값 변경 시 EventSink.OnButton(id) 발동, DSL 이벤트 핸들러 반응형 처리 가능.
  • [개선] DuxelAppOptions.Screen — nullable optional에서 required로 변경, 폐기된 DuxelDslOptions 진입점 및 UiDslBindings 클래스 제거.
  • [개선] ProgressBar accent 색상 — ProgressBarFillSliderGrab 대신 PlotHistogram 사용 (Dear ImGui 기준 일치).

주요 버그 수정

  • [버그] Text Bind 위치 인자 간섭 — ReadOptionalString("Bind")가 텍스트 내용까지 소비하던 문제, ReadNamedString("Bind", null)로 수정.
  • [버그] 제어 흐름 스킵 경로 스타일 leak — 스킵된 컨테이너의 EndNode에서 StyleColorPushCounts 미정리 수정.
  • [버그] Selectable 이벤트 누락 — Selectable 상태 변경 시 OnButtonOnCheckbox 이벤트 모두 발동하도록 수정.

| Main.ui

Window "Theme Hot-Reload Demo"
  MenuBar
    Menu "File"
      MenuItem "file.new" "New"
      MenuItem "file.open" "Open"
      MenuItem "file.save" "Save"
    Menu "Edit"
      MenuItem "edit.undo" "Undo"
      MenuItem "edit.redo" "Redo"
    Menu "View"
      MenuItem "view.theme" "Theme Info"

  Text Bind="status" Default="Ready"
  Separator

  SeparatorText "Theme"
  Combo "theme" "Theme" Items="Dark,Light,Classic,Nord,Solarized Dark,Solarized Light,Dracula,Monokai,Catppuccin Mocha,GitHub Dark"
  Separator

  SeparatorText "Buttons"
  Row
    Button "primary" "Primary Action"
    Button "secondary" "Small" Size=80,0
    ArrowButton "arrow_r" Right

  SeparatorText "Toggles"
  Checkbox "neon" "Enable Neon Glow" true
  Checkbox "wireframe" "Wireframe Mode" false
  Row
    RadioButton "opt" "Option A" 0
    RadioButton "opt" "Option B" 1
    RadioButton "opt" "Option C" 2

  SeparatorText "Input"
  InputText "name" "Name" "Hello Cyberpunk"

  SeparatorText "Sliders & Drags"
  SliderFloat "intensity" "Intensity" 0.0 1.0
  DragFloat "value" "Value" 42.0 0.5 0.0 100.0
  ProgressBar 0.72 Size=0,0 Overlay="Loading..."

  SeparatorText "Selection"
  Combo "style" "Style" "Neon"
    Selectable "style.neon" "Neon"
    Selectable "style.vapor" "Vapor"
    Selectable "style.retro" "Retro"
    Selectable "style.synth" "Synth"

  ListBox "channels" "Channels" "Alpha"
    Selectable "ch.alpha" "Alpha"
    Selectable "ch.bravo" "Bravo"
    Selectable "ch.charlie" "Charlie"
    Selectable "ch.delta" "Delta"

  SeparatorText "Tree & Collapsing"
  CollapsingHeader "System Info"
    TreeNode "GPU"
      Text "Vendor: Cyberdyne"
      Text "VRAM: 16 GB"
    TreeNode "Display"
      Text "Resolution: 3840x2160"
      Text "Refresh: 144 Hz"

  SeparatorText "Tabs"
  TabBar "DemoTabs"
    TabItem "Overview"
      Text "Theme overview panel."
    TabItem "Settings"
      Text "Adjust theme parameters here."

  SeparatorText "Selectable"
  Selectable "profile.1" "Profile 1"
  Selectable "profile.2" "Profile 2"
  Selectable "profile.3" "Profile 3"

  SeparatorText "Tooltip Demo"
  Button "hover_me" "Hover Me"
  Text "Edit cyberpunk.duxel-theme to see hot-reload!"

  # ── Control Flow Demo (self-contained) ─────────────────
  SeparatorText "Control Flow: If / Else"
  Checkbox "cf_alpha" "Alpha" true
  Checkbox "cf_beta" "Beta" false
  If Bind="cf_alpha"
    Text "Alpha is ON"
  ElseIf Bind="cf_beta"
    Text "Beta is ON"
  Else
    Text "Both OFF"

  SeparatorText "Control Flow: Visible"
  Checkbox "cf_show" "Show hidden section" false
  Visible Bind="cf_show"
    Text "This text is only visible when the checkbox above is checked"

  SeparatorText "Control Flow: ForEach"
  ForEach Range=1,5
    Button "item_{_index}" "Item {_index}"

  SeparatorText "Control Flow: Switch / Case"
  Selectable "cf_mode.A" "Mode A"
  Selectable "cf_mode.B" "Mode B"
  Selectable "cf_mode.C" "Mode C"
  Switch Bind="cf_mode"
    Case "A"
      Text "Mode A selected"
    Case "B"
      Text "Mode B selected"
    Case "C"
      Text "Mode C selected"
    Default
      Text "No mode selected — click one above"

| cyberpunk.duxel-theme

Theme "Cyberpunk" : Dark

// ── Global palette ──
WindowBg = #0D0D1A
Text = #E0E0F0
TextDisabled = #55557A
Border = #2A1A4A

// ── Window ──
TitleBg = #0A0A18
TitleBgActive = #1A0A2A
WindowTitleText = #00FFCC

// ── Buttons ──
Button = #1A1A3A
ButtonHovered = #2A2A5A
ButtonActive = #00CCAA
ButtonBorder = #00FFCC
ButtonText = #00FFCC

// ── Checkbox ──
FrameBg = #1A1A3A
FrameBgHovered = #2A2A5A
FrameBgActive = #00CCAA
CheckMark = #00FFCC
CheckboxBg = #1A1A3A
CheckboxBgHovered = #2A2A5A
CheckboxBgActive = #00CCAA
CheckboxText = #E0E0F0

// ── Radio Button ──
RadioButtonBg = #1A1A3A
RadioButtonBgHovered = #2A2A5A
RadioButtonBgActive = #00CCAA
RadioButtonText = #E0E0F0

// ── Progress Bar ──
ProgressBarBg = #1A1A3A
ProgressBarFill = #00FFCC

// ── Input ──
InputBg = #12122A
InputBgHovered = #1A1A3A
InputText = #E0E0F0
InputSelectionBg = #4400CCAA

// ── Slider ──
SliderBg = #1A1A3A
SliderBgHovered = #2A2A5A
SliderGrab = #00FFCC
SliderGrabActive = #00EEBB
SliderText = #E0E0F0

// ── Drag ──
DragBg = #1A1A3A
DragBgHovered = #2A2A5A
DragBgActive = #00CCAA
DragText = #E0E0F0

// ── Combo ──
ComboBg = #1A1A3A
ComboBgHovered = #2A2A5A
ComboBgActive = #00CCAA
ComboPopupBg = #12122A
ComboText = #E0E0F0

// ── ListBox ──
ListBoxBg = #12122A
ListBoxItemBgActive = #00CCAA
ListBoxItemBgHovered = #2A2A5A
ListBoxItemText = #E0E0F0

// ── Selectable ──
SelectableBgActive = #00CCAA
SelectableBgHovered = #2A2A5A
SelectableText = #E0E0F0

// ── Menu ──
MenuBarBg = #0A0A18
PopupBg = #12122A
MenuItemBgActive = #00CCAA
MenuItemBgHovered = #2A2A5A
MenuItemText = #E0E0F0
MenuItemTextDisabled = #55557A

// ── Tree ──
TreeNodeBg = #1A1A3A
TreeNodeBgActive = #00CCAA
TreeNodeBgHovered = #2A2A5A
TreeNodeText = #E0E0F0

// ── Tab ──
Tab = #1A1A3A
TabHovered = #2A2A5A
TabActive = #2A1A4A
TabText = #E0E0F0

// ── Table ──
TableHeaderBg = #1A1A3A
TableBorder = #2A1A4A
TableRowBg0 = #0D0D1A
TableRowBg1 = #12122A
TableHeaderText = #00FFCC

// ── Tooltip ──
TooltipBg = #12122A
TooltipText = #E0E0F0

// ── Separator ──
Separator = #2A1A4A
SeparatorLabelText = #00FFCC

// ── Scrollbar ──
ScrollbarBg = #0A0A18
ScrollbarGrab = #2A2A5A
ScrollbarGrabHovered = #3A3A6A
ScrollbarGrabActive = #00CCAA
3개의 좋아요