[0.0.0] Capture viewer: a grid of every .mcap's preview, and one capture at full size in a window of its own

This commit is contained in:
Musa Mahmood 2026-10-07 11:41:38 +01:00
commit 4323831801
472 changed files with 395096 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
# Build output: binaries, copied assets and logs go to bin/, intermediates to .build/ (the
# binding generators in modules/ make their own).
/bin/
.build/
# Debuggers and editors
*.rdi
.vs/
.vscode/
*.10x
# fork.jai writes one with this machine's paths
*.focus-config
# OS and sync clutter
.DS_Store
Thumbs.db
.stfolder

125
README.md Normal file
View File

@ -0,0 +1,125 @@
# mcap
A bare [Dear ImGui](https://github.com/ocornut/imgui) (docking branch) desktop application
written in [Jai](https://jai.community), for Windows, macOS and Linux: the window, the frame loop
and everything around them, ready for an app to be written into `src/app/app_ui.jai`.
## What you get
- **Native rendering per platform.** Win32 + Direct3D 11 on Windows; SDL3 + SDL_GPU (Metal on
macOS, Vulkan on Linux) elsewhere. On Windows the window comes up on a WARP device in a few
milliseconds while the hardware device is created on a thread, then moves across; a lost
device is recovered.
- **Docking and multi-viewport ImGui**, with a default layout (`assets/data/default_imgui.ini`)
and a Reset Layout hotkey.
- **One instance.** Launching the app again brings the running one forward, over a shared
memory channel (a file lock decides the owner on macOS and Linux).
- **Tray / menu bar.** Closing or minimizing hides the window to the tray; the tray icon brings
it back or quits.
- **Hotkeys** from a `[hotkeys]` section of the settings file, and **global hotkeys**
(`[global_hotkeys]`: Windows and macOS) that work while another app is in front.
- **A settings file** (`settings.config`) that is written with every key listed, reloaded as
soon as it is saved, and merged rather than overwritten when the app saves to it.
- **Themes**: built-ins, 36 shipped `.theme` files, and a theme editor that saves new ones.
- **Logging** through a multi-producer, single-consumer queue to a per-version log file and an
in-app Event Log, plus crash reports.
- **Debug panels**: frame times, memory, renderer, fonts (Ctrl+Shift+D, Ctrl+Shift+1).
- **DPI awareness**, live resize that keeps drawing, CJK font fallback from the system fonts.
- **Background shutdown**: closing hands the remaining work to a thread while the window shows
progress, so it never looks hung.
- **Asynchronous tests**, compiled into debug builds and run with `run_tests`.
## Building
Requires the Jai compiler, beta 0.2.030 or later.
```
jai first.jai # debug build
jai first.jai - release # release build
jai first.jai - full_release # debug and release
jai first.jai - release compile_tests # release build with the test suite
```
Binaries are written to `bin/`, with `assets/` copied beside them.
| Platform | Output | Notes |
| --- | --- | --- |
| Windows | `bin/mcap-debug.exe`, `bin/mcap.exe` | The release build gets the icon and a manifest, and no console. |
| macOS | `bin/mcap-macos-debug`, `bin/mcap.app` | Apple silicon (arm64) only, with a native `jai`. The release build is an ad hoc signed `.app`. |
| Linux | `bin/mcap-linux-debug`, `bin/mcap-linux` | Needs SDL3 installed (`libSDL3.so`). |
`jai check_targets.jai - linux` type-checks the Linux build from any OS without linking it.
The macOS check only works on a Mac, since the Objective-C module runs code at compile time.
Run any build with `run_tests` to run the test suites (debug builds include them),
`-allow-multiple-instances` to skip the single-instance check, or `-version`.
## Starting a new app
```
jai fork.jai - <output_directory> <app_name> [-folder <name>] [-bundle-id <id>] [-version <x.y.z>] [-git] [-build]
jai fork.jai - ../my-cool-app "My Cool App" -git -build
```
This copies the template (without `bin/`, `.build/`, `.git/` or `fork.jai`) and sets the names
below in the copy. The folder name defaults to the app name in lowercase with spaces as `-`
(`my-cool-app`), the bundle identifier to `com.example.<folder without dashes>`, and the version
to `0.0.0`. `-git` initializes a repository and commits the copy; `-build` makes a debug build. It also
writes `<app_name>.focus-config`, a [focus](https://focus-editor.dev) project with the build and
run commands, which stays out of git since it holds this machine's paths.
## Making it your app
1. Name it at the top of `first.jai`, or let `fork.jai` do it: `APP_NAME` (window title, tray,
macOS bundle), `APP_FOLDER_NAME` (executable and data folder; one short lowercase word),
`APP_BUNDLE_IDENTIFIER` and `VERSION`.
2. Replace `assets/images/icon.png` (any square size; it becomes the `.exe`, `.app` and window
icon) and the tray icons `tray_icon.ico` (Windows) and `tray_icon.png`.
3. Write the app in `src/app/app_ui.jai`: `app_ui_startup` runs once, `app_ui_frame_start` every
frame before input, `app_main_window_ui` draws, and `app_ui_shutdown` runs on the shutdown
thread.
To add a hotkey, declare a `Hotkey` in `src/ui/imgui_keyboard.jai`, add it to
`hotkey_actions_build` and `app_check_key_combinations`, bind it in both
`assets/data/default_hotkeys*.config` files (the build fails if a default leaves an action
unbound), and raise `APP_USER_SETTINGS_VERSION` in `src/App.jai` so existing settings files get
the new default.
To add a setting, add it to `App_User_Settings`, `Settings_Seen`, `settings_parse` and
`app_save_user_settings_file` in `src/ui/imgui_theme_registry.jai`.
## Layout
```
first.jai build: workspaces, build constants, Windows icon/manifest, macOS .app
fork.jai copy the template to a new, named project
check_targets.jai type-check another OS's build
src/App.jai loads everything; global state (app, platform, perf)
src/app/ main loop, your UI, background shutdown
src/base/ logging, fonts, debug panels, platform layers (base/platform/)
src/ui/ ImGui setup, hotkeys, themes and settings, toasts
src/graphics/DX11.jai Direct3D 11 device, WARP-first startup, device-loss recovery
src/backends/ ImGui's SDL_GPU backend; src/freetype/: ImGui's FreeType loader
assets/ fonts, icons, default hotkeys, default layout, themes
modules/ Basic (with arenas), Thread, Shared_Memory_Channel, File_Watcher,
Secure_Memory, XDG, sdl3, imgui-sdl, imgui-dx11-win32, imgui-source
```
## Files at run time
| File | Location |
| --- | --- |
| `settings.config`, `imgui.ini`, user themes | `%APPDATA%\<APP_FOLDER_NAME>` on Windows, `~/Library/Application Support/<APP_FOLDER_NAME>` on macOS, `~/.local/share/<APP_FOLDER_NAME>` on Linux |
| Logs | `logs/<version>/` next to the executable (next to the `.app` on macOS) |
| Shipped themes | `data/themes/` next to the executable |
## Platform notes
- Global hotkeys are not implemented on Linux (Wayland has no API for them). A desktop
shortcut that launches the executable has the same effect, since a second launch brings the
running instance forward.
- On XFCE the tray icon stays after the window is restored.
- `Secure_Memory` (used by `Basic`'s arenas to wipe memory) is built without libsodium; see the
note at the top of `modules/Secure_Memory/module.jai` if your app keeps secrets in memory.
Uses https://github.com/overlord-systems/jai-imgui as a template.

View File

@ -0,0 +1,8 @@
[global_hotkeys]
# Registered with the system, so they work from any application, even while the app is hidden in the tray.
# The format is that of [hotkeys], including "none" to turn an action off; keys are letters,
# digits, Space, F1-F24, Enter, Tab, Escape, the arrows, PageUp, PageDown, Home, End, Insert and
# Delete. For example: Ctrl-Alt-Space bring_to_foreground
# Changes apply as soon as the file is saved.
none bring_to_foreground

View File

@ -0,0 +1,7 @@
[global_hotkeys]
# Registered with the system, so they work from any application, even while the app is hidden in the menu bar.
# The format is that of [hotkeys], including "none" to turn an action off; keys are letters,
# digits, Space, F1-F12, Enter, Tab, Escape and the arrows. For example: Cmd-Shift-Space bring_to_foreground
# Changes apply as soon as the file is saved.
none bring_to_foreground

View File

@ -0,0 +1,25 @@
[hotkeys]
# Each line is a key combination and the action it triggers. Modifiers are Ctrl, Shift and Alt,
# joined to the key with '-': Ctrl-Shift-W. Keys are letters, digits, F1-F24, Enter, Escape,
# Delete, PageUp, PageDown, UpArrow/ArrowUp, Minus, Equal and the other Dear ImGui key names.
# List an action on several lines to bind several combinations. Write "none" instead of a key
# combination to turn an action off; leaving it out entirely instead means the app hasn't added it
# to your settings file yet, so the next save fills in its default (or "none", while upgrading an
# older settings file, so nothing changes for you until you ask).
# Changes apply as soon as the file is saved.
# Key combination Action
# Window
Ctrl-Shift-W program_exit
Ctrl-W minimize_to_tray
Ctrl-Minus decrease_font_size
Ctrl-Equal increase_font_size
Ctrl-Alt-Space reset_window
Ctrl-Shift-Alt-R reset_layout
# Debug
Ctrl-Shift-D toggle_debug_panel
Ctrl-Shift-L toggle_logger_panel
Ctrl-Shift-E toggle_style_editor
Ctrl-Shift-1 toggle_frame_times

View File

@ -0,0 +1,25 @@
[hotkeys]
# Each line is a key combination and the action it triggers. Modifiers are Cmd, Shift and Opt (Option),
# joined to the key with '-': Cmd-Shift-W. Keys are letters, digits, F1-F24, Enter, Escape,
# Delete, PageUp, PageDown, UpArrow/ArrowUp, Minus, Equal and the other Dear ImGui key names.
# List an action on several lines to bind several combinations. Write "none" instead of a key
# combination to turn an action off; leaving it out entirely instead means the app hasn't added it
# to your settings file yet, so the next save fills in its default (or "none", while upgrading an
# older settings file, so nothing changes for you until you ask).
# Changes apply as soon as the file is saved.
# Key combination Action
# Window
Cmd-Shift-W program_exit
Cmd-W minimize_to_tray
Cmd-Minus decrease_font_size
Cmd-Equal increase_font_size
Cmd-Opt-Shift-Space reset_window
Cmd-Shift-Opt-R reset_layout
# Debug
Cmd-Shift-D toggle_debug_panel
Cmd-Shift-L toggle_logger_panel
Cmd-Shift-E toggle_style_editor
Cmd-Shift-1 toggle_frame_times

View File

@ -0,0 +1,38 @@
[Window][WindowOverViewport_11111111]
Pos=0,0
Size=1600,900
Collapsed=0
[Window][Main]
Pos=0,0
Size=1100,640
Collapsed=0
DockId=0x00000003,0
[Window][Event Log]
Pos=0,644
Size=800,256
Collapsed=0
DockId=0x00000005,0
[Window][Frame Times]
Pos=804,644
Size=296,256
Collapsed=0
DockId=0x00000006,0
[Window][Debug Panel]
Pos=1104,0
Size=496,900
Collapsed=0
DockId=0x00000002,0
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,0 Size=1600,900 Split=X
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1100,900 Split=Y
DockNode ID=0x00000003 Parent=0x00000001 SizeRef=1100,640 CentralNode=1
DockNode ID=0x00000004 Parent=0x00000001 SizeRef=1100,256 Split=X
DockNode ID=0x00000005 Parent=0x00000004 SizeRef=800,256
DockNode ID=0x00000006 Parent=0x00000004 SizeRef=296,256 HiddenTabBar=1
DockNode ID=0x00000002 Parent=0x08BD597D SizeRef=496,900

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Basic Light
@base default
[colors]
Text: 181818FF
TextDisabled: 383838FF
WindowBg: FFFFFFFF
ChildBg: CCCCCCFF
PopupBg: CCCCCCFF
Border: 18181880
BorderShadow: 00000000
FrameBg: FFFFFFFF
FrameBgHovered: E3E3E3FF
FrameBgActive: CCCCCCFF
TitleBg: CCCCCCFF
TitleBgActive: BABABAFF
TitleBgCollapsed: CCCCCCFF
MenuBarBg: CCCCCCFF
ScrollbarBg: CCCCCCFF
ScrollbarGrab: D1D1D1FF
ScrollbarGrabHovered: B0B0B0FF
ScrollbarGrabActive: 909090FF
CheckMark: 6F0093FF
CheckboxSelectedBg: D4B3DFFF
SliderGrab: 6F0093B0
SliderGrabActive: 6F0093FF
Button: FFFFFFFF
ButtonHovered: DFDFDFFF
ButtonActive: C599D4FF
Header: E8E8E8FF
HeaderHovered: D5D5D5FF
HeaderActive: CDA6D9FF
Separator: 18181860
SeparatorHovered: FCFCFCFF
SeparatorActive: 6F0093FF
ResizeGrip: 18181860
ResizeGripHovered: 6F0093A0
ResizeGripActive: 6F0093FF
InputTextCursor: 181818FF
TabHovered: DADADAFF
Tab: CCCCCCFF
TabSelected: E8E8E8FF
TabSelectedOverline: 6F0093FF
TabDimmed: FFFFFFFF
TabDimmedSelected: CCCCCCFF
TabDimmedSelectedOverline: 181818FF
DockingPreview: 6F009370
DockingEmptyBg: FFFFFFFF
PlotLines: 6F0093FF
PlotLinesHovered: E6FEFEFF
PlotHistogram: 0000FFFF
PlotHistogramHovered: E6FEFEFF
TableHeaderBg: BEBEBEFF
TableBorderStrong: 18181880
TableBorderLight: E6FEFE26
TableRowBg: 00000000
TableRowBgAlt: 1818180D
TextLink: 6F0093FF
TextSelectedBg: B6B6B6FF
TreeLines: E6FEFE26
DragDropTarget: AC600CFF
DragDropTargetBg: AC600C30
UnsavedMarker: AC600CFF
NavCursor: 6F0093FF
NavWindowingHighlight: 181818B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 6F0093FF
accent_alt: 181818FF
error: FF0000FF
error_dim: 772222FF
warning: AC600CFF
warning_dim: FFA500FF
success: 6DA465FF
dim: 383838FF
neutral: 181818FF
active_window_border: 6F0093FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Candy 2
@base default
[colors]
Text: 9DD2DEFF
TextDisabled: 87919DFF
WindowBg: 15212AFF
ChildBg: 10191FFF
PopupBg: 10191FFF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 18262FFF
FrameBgHovered: 283B44FF
FrameBgActive: 354C56FF
TitleBg: 10191FFF
TitleBgActive: 1E2C32FF
TitleBgCollapsed: 10191FFF
MenuBarBg: 10191FFF
ScrollbarBg: 10191FFF
ScrollbarGrab: 334852FF
ScrollbarGrabHovered: 45606BFF
ScrollbarGrabActive: 587983FF
CheckMark: 5C9FD1FF
CheckboxSelectedBg: 2C4A60FF
SliderGrab: 5C9FD1B0
SliderGrabActive: 5C9FD1FF
Button: 18262FFF
ButtonHovered: 2B3E48FF
ButtonActive: 335670FF
Header: 23333CFF
HeaderHovered: 2D414AFF
HeaderActive: 2E4D64FF
Separator: 4C4C4C60
SeparatorHovered: 1C4449FF
SeparatorActive: 5C9FD1FF
ResizeGrip: 4C4C4C60
ResizeGripHovered: 5C9FD1A0
ResizeGripActive: 5C9FD1FF
InputTextCursor: 26B2B2FF
TabHovered: 2D424BFF
Tab: 10191FFF
TabSelected: 253741FF
TabSelectedOverline: 5C9FD1FF
TabDimmed: 15212AFF
TabDimmedSelected: 10191FFF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: 5C9FD170
DockingEmptyBg: 15212AFF
PlotLines: 5C9FD1FF
PlotLinesHovered: F4F492FF
PlotHistogram: 9DD2DEFF
PlotHistogramHovered: F4F492FF
TableHeaderBg: 1B282EFF
TableBorderStrong: 4C4C4C80
TableBorderLight: FCEDFC26
TableRowBg: 00000000
TableRowBgAlt: 9DD2DE0D
TextLink: 5C9FD1FF
TextSelectedBg: 1C4449FF
TreeLines: FCEDFC26
DragDropTarget: F8AD34FF
DragDropTargetBg: F8AD3430
UnsavedMarker: F8AD34FF
NavCursor: 5C9FD1FF
NavWindowingHighlight: 9DD2DEB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 5C9FD1FF
accent_alt: FFB7C4FF
error: FF0000FF
error_dim: 772222FF
warning: F8AD34FF
warning_dim: 986032FF
success: 227722FF
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: 5C9FD1FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Candy
@base default
[colors]
Text: BFC9DBFF
TextDisabled: 87919DFF
WindowBg: 15212AFF
ChildBg: 10191FFF
PopupBg: 10191FFF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 18262FFF
FrameBgHovered: 2C3A44FF
FrameBgActive: 3D4A55FF
TitleBg: 10191FFF
TitleBgActive: 222B32FF
TitleBgCollapsed: 10191FFF
MenuBarBg: 10191FFF
ScrollbarBg: 10191FFF
ScrollbarGrab: 394751FF
ScrollbarGrabHovered: 515D69FF
ScrollbarGrabActive: 687482FF
CheckMark: E03D90FF
CheckboxSelectedBg: 542D4CFF
SliderGrab: E03D90B0
SliderGrabActive: E03D90FF
Button: 18262FFF
ButtonHovered: 2F3D47FF
ButtonActive: 682F56FF
Header: 26323CFF
HeaderHovered: 343F4AFF
HeaderActive: 5C2B4EFF
Separator: 4C4C4C60
SeparatorHovered: 1C4449FF
SeparatorActive: E03D90FF
ResizeGrip: 4C4C4C60
ResizeGripHovered: E03D90A0
ResizeGripActive: E03D90FF
InputTextCursor: 26B2B2FF
TabHovered: 33404BFF
Tab: 10191FFF
TabSelected: 293640FF
TabSelectedOverline: E03D90FF
TabDimmed: 15212AFF
TabDimmedSelected: 10191FFF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: E03D9070
DockingEmptyBg: 15212AFF
PlotLines: E03D90FF
PlotLinesHovered: D89B75FF
PlotHistogram: 6CCFE6FF
PlotHistogramHovered: D89B75FF
TableHeaderBg: 1E272EFF
TableBorderStrong: 4C4C4C80
TableBorderLight: FCEDFC26
TableRowBg: 00000000
TableRowBgAlt: BFC9DB0D
TextLink: E03D90FF
TextSelectedBg: 1C4449FF
TreeLines: FCEDFC26
DragDropTarget: F8AD34FF
DragDropTargetBg: F8AD3430
UnsavedMarker: F8AD34FF
NavCursor: E03D90FF
NavWindowingHighlight: BFC9DBB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: E03D90FF
accent_alt: F5B0EDFF
error: FF0000FF
error_dim: 772222FF
warning: F8AD34FF
warning_dim: 986032FF
success: 227722FF
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: F5B0EDFF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Catppuccin Frappe
@base default
[colors]
Text: C6D0F5FF
TextDisabled: A5ADCEFF
WindowBg: 303446FF
ChildBg: 292C3CFF
PopupBg: 292C3CFF
Border: A5ADCE80
BorderShadow: 00000000
FrameBg: 232634FF
FrameBgHovered: 373A4BFF
FrameBgActive: 474B5EFF
TitleBg: 292C3CFF
TitleBgActive: 393C4FFF
TitleBgCollapsed: 292C3CFF
MenuBarBg: 292C3CFF
ScrollbarBg: 292C3CFF
ScrollbarGrab: 44485BFF
ScrollbarGrabHovered: 5A6076FF
ScrollbarGrabActive: 717891FF
CheckMark: CA9EE6FF
CheckboxSelectedBg: 554A69FF
SliderGrab: CA9EE6B0
SliderGrabActive: CA9EE6FF
Button: 232634FF
ButtonHovered: 3A3E4FFF
ButtonActive: 66567BFF
Header: 3F4458FF
HeaderHovered: 4B5066FF
HeaderActive: 66597EFF
Separator: A5ADCE60
SeparatorHovered: 51576DFF
SeparatorActive: CA9EE6FF
ResizeGrip: A5ADCE60
ResizeGripHovered: CA9EE6A0
ResizeGripActive: CA9EE6FF
InputTextCursor: 626880FF
TabHovered: 3D4153FF
Tab: 292C3CFF
TabSelected: 333747FF
TabSelectedOverline: CA9EE6FF
TabDimmed: 303446FF
TabDimmedSelected: 292C3CFF
TabDimmedSelectedOverline: A5ADCEFF
DockingPreview: CA9EE670
DockingEmptyBg: 303446FF
PlotLines: CA9EE6FF
PlotLinesHovered: 51576DFF
PlotHistogram: EF9F76FF
PlotHistogramHovered: 51576DFF
TableHeaderBg: 36394BFF
TableBorderStrong: A5ADCE80
TableBorderLight: 414559FF
TableRowBg: 00000000
TableRowBgAlt: C6D0F50D
TextLink: CA9EE6FF
TextSelectedBg: 51576DFF
TreeLines: 414559FF
DragDropTarget: E5C890FF
DragDropTargetBg: E5C89030
UnsavedMarker: E5C890FF
NavCursor: CA9EE6FF
NavWindowingHighlight: C6D0F5B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: CA9EE6FF
accent_alt: 8CAAEEFD
error: E78284FF
error_dim: E78284FF
warning: E5C890FF
warning_dim: E5C890FF
success: A6D189FF
dim: A5ADCEFF
neutral: A5ADCEFF
active_window_border: CA9EE6FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Catppuccin Latte
@base default
[colors]
Text: 4C4F69FF
TextDisabled: 6C6F85FF
WindowBg: EFF1F5FF
ChildBg: E6E9EFFF
PopupBg: E6E9EFFF
Border: 6C6F8580
BorderShadow: 00000000
FrameBg: DCE0E8FF
FrameBgHovered: CBCFD9FF
FrameBgActive: BCC0CCFF
TitleBg: E6E9EFFF
TitleBgActive: D7DAE2FF
TitleBgCollapsed: E6E9EFFF
MenuBarBg: E6E9EFFF
ScrollbarBg: E6E9EFFF
ScrollbarGrab: BFC3CFFF
ScrollbarGrabHovered: ABAFBDFF
ScrollbarGrabActive: 979AABFF
CheckMark: 8839EFFF
CheckboxSelectedBg: C3AEEAFF
SliderGrab: 8839EFB0
SliderGrabActive: 8839EFFF
Button: DCE0E8FF
ButtonHovered: C8CCD6FF
ButtonActive: BA9DEBFF
Header: DFE1E7FF
HeaderHovered: D2D4DCFF
HeaderActive: CBB1F3FF
Separator: 6C6F8560
SeparatorHovered: BCC0CCFF
SeparatorActive: 8839EFFF
ResizeGrip: 6C6F8560
ResizeGripHovered: 8839EFA0
ResizeGripActive: 8839EFFF
InputTextCursor: ACB0BEFF
TabHovered: C5C9D4FF
Tab: E6E9EFFF
TabSelected: CED2DBFF
TabSelectedOverline: 8839EFFF
TabDimmed: EFF1F5FF
TabDimmedSelected: E6E9EFFF
TabDimmedSelectedOverline: 6C6F85FF
DockingPreview: 8839EF70
DockingEmptyBg: EFF1F5FF
PlotLines: 8839EFFF
PlotLinesHovered: BCC0CCFF
PlotHistogram: FE640BFF
PlotHistogramHovered: BCC0CCFF
TableHeaderBg: DADDE4FF
TableBorderStrong: 6C6F8580
TableBorderLight: CCD0DAFF
TableRowBg: 00000000
TableRowBgAlt: 4C4F690D
TextLink: 8839EFFF
TextSelectedBg: BCC0CCFF
TreeLines: CCD0DAFF
DragDropTarget: DF8E1DFF
DragDropTargetBg: DF8E1D30
UnsavedMarker: DF8E1DFF
NavCursor: 8839EFFF
NavWindowingHighlight: 4C4F69B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 8839EFFF
accent_alt: 1E66F5FF
error: D20F39FF
error_dim: D20F39FF
warning: DF8E1DFF
warning_dim: DF8E1DFF
success: 40A02BFF
dim: 6C6F85FF
neutral: 6C6F85FF
active_window_border: 8839EFFF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Catppuccin Macchiato
@base default
[colors]
Text: CAD3F5FF
TextDisabled: A5ADCBFF
WindowBg: 24273AFF
ChildBg: 1E2030FF
PopupBg: 1E2030FF
Border: A5ADCB80
BorderShadow: 00000000
FrameBg: 181926FF
FrameBgHovered: 2D2F3FFF
FrameBgActive: 3F4254FF
TitleBg: 1E2030FF
TitleBgActive: 2F3244FF
TitleBgCollapsed: 1E2030FF
MenuBarBg: 1E2030FF
ScrollbarBg: 1E2030FF
ScrollbarGrab: 3C3E4FFF
ScrollbarGrabHovered: 55586CFF
ScrollbarGrabActive: 6D7289FF
CheckMark: C6A0F6FF
CheckboxSelectedBg: 4C4264FF
SliderGrab: C6A0F6B0
SliderGrabActive: C6A0F6FF
Button: 181926FF
ButtonHovered: 313343FF
ButtonActive: 5E4F79FF
Header: 35384DFF
HeaderHovered: 42465CFF
HeaderActive: 5D517CFF
Separator: A5ADCB60
SeparatorHovered: 494D64FF
SeparatorActive: C6A0F6FF
ResizeGrip: A5ADCB60
ResizeGripHovered: C6A0F6A0
ResizeGripActive: C6A0F6FF
InputTextCursor: 5B6078FF
TabHovered: 343747FF
Tab: 1E2030FF
TabSelected: 2A2C3BFF
TabSelectedOverline: C6A0F6FF
TabDimmed: 24273AFF
TabDimmedSelected: 1E2030FF
TabDimmedSelectedOverline: A5ADCBFF
DockingPreview: C6A0F670
DockingEmptyBg: 24273AFF
PlotLines: C6A0F6FF
PlotLinesHovered: 494D64FF
PlotHistogram: F5A97FFF
PlotHistogramHovered: 494D64FF
TableHeaderBg: 2C2E40FF
TableBorderStrong: A5ADCB80
TableBorderLight: 363A4FFF
TableRowBg: 00000000
TableRowBgAlt: CAD3F50D
TextLink: C6A0F6FF
TextSelectedBg: 494D64FF
TreeLines: 363A4FFF
DragDropTarget: EED49FFF
DragDropTargetBg: EED49F30
UnsavedMarker: EED49FFF
NavCursor: C6A0F6FF
NavWindowingHighlight: CAD3F5B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: C6A0F6FF
accent_alt: 8AADF4FF
error: ED8796FF
error_dim: ED8796FF
warning: EED49FFF
warning_dim: EED49FFF
success: A6DA95FF
dim: A5ADCBFF
neutral: A5ADCBFF
active_window_border: C6A0F6FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Catppuccin Mocha
@base default
[colors]
Text: CDD6F4FF
TextDisabled: 9DA0ABFF
WindowBg: 1E1E2EFF
ChildBg: 181825FF
PopupBg: 181825FF
Border: 9DA0AB80
BorderShadow: 00000000
FrameBg: 11111BFF
FrameBgHovered: 282935FF
FrameBgActive: 3A3C4BFF
TitleBg: 181825FF
TitleBgActive: 2A2B3AFF
TitleBgCollapsed: 181825FF
MenuBarBg: 181825FF
ScrollbarBg: 181825FF
ScrollbarGrab: 373846FF
ScrollbarGrabHovered: 515465FF
ScrollbarGrabActive: 6B7083FF
CheckMark: C6A0F6FF
CheckboxSelectedBg: 473C5DFF
SliderGrab: C6A0F6B0
SliderGrabActive: C6A0F6FF
Button: 11111BFF
ButtonHovered: 2B2D39FF
ButtonActive: 594A73FF
Header: 303042FF
HeaderHovered: 3E3F52FF
HeaderActive: 594C74FF
Separator: 9DA0AB60
SeparatorHovered: 45475AFF
SeparatorActive: C6A0F6FF
ResizeGrip: 9DA0AB60
ResizeGripHovered: C6A0F6A0
ResizeGripActive: C6A0F6FF
InputTextCursor: B5A6A8FF
TabHovered: 2F313EFF
Tab: 181825FF
TabSelected: 242531FF
TabSelectedOverline: C6A0F6FF
TabDimmed: 1E1E2EFF
TabDimmedSelected: 181825FF
TabDimmedSelectedOverline: 9DA0ABFF
DockingPreview: C6A0F670
DockingEmptyBg: 1E1E2EFF
PlotLines: C6A0F6FF
PlotLinesHovered: 494D64FF
PlotHistogram: F5A97FFF
PlotHistogramHovered: 494D64FF
TableHeaderBg: 262736FF
TableBorderStrong: 9DA0AB80
TableBorderLight: 313244FF
TableRowBg: 00000000
TableRowBgAlt: CDD6F40D
TextLink: C6A0F6FF
TextSelectedBg: 45475AFF
TreeLines: 313244FF
DragDropTarget: F9E2AFFF
DragDropTargetBg: F9E2AF30
UnsavedMarker: F9E2AFFF
NavCursor: C6A0F6FF
NavWindowingHighlight: CDD6F4B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: C6A0F6FF
accent_alt: 89B4FAFF
error: F38BA8FF
error_dim: F38BA8FF
warning: F9E2AFFF
warning_dim: F9E2AFFF
success: A6E3A1FF
dim: 9DA0ABFF
neutral: 9DA0ABFF
active_window_border: C6A0F6FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Dsp Lib
@base default
[colors]
Text: 9DD2DEFF
TextDisabled: 87919DFF
WindowBg: 15212AFF
ChildBg: 10191FFF
PopupBg: 10191FFF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 18262FFF
FrameBgHovered: 283B44FF
FrameBgActive: 354C56FF
TitleBg: 10191FFF
TitleBgActive: 1E2C32FF
TitleBgCollapsed: 10191FFF
MenuBarBg: 10191FFF
ScrollbarBg: 10191FFF
ScrollbarGrab: 334852FF
ScrollbarGrabHovered: 45606BFF
ScrollbarGrabActive: 587983FF
CheckMark: 5C9FD1FF
CheckboxSelectedBg: 2C4A60FF
SliderGrab: 5C9FD1B0
SliderGrabActive: 5C9FD1FF
Button: 18262FFF
ButtonHovered: 2B3E48FF
ButtonActive: 335670FF
Header: 23333CFF
HeaderHovered: 2D414AFF
HeaderActive: 2E4D64FF
Separator: 4C4C4C60
SeparatorHovered: 1C4449FF
SeparatorActive: 5C9FD1FF
ResizeGrip: 4C4C4C60
ResizeGripHovered: 5C9FD1A0
ResizeGripActive: 5C9FD1FF
InputTextCursor: 26B2B2FF
TabHovered: 2D424BFF
Tab: 10191FFF
TabSelected: 253741FF
TabSelectedOverline: 5C9FD1FF
TabDimmed: 15212AFF
TabDimmedSelected: 10191FFF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: 5C9FD170
DockingEmptyBg: 15212AFF
PlotLines: 5C9FD1FF
PlotLinesHovered: F4F492FF
PlotHistogram: 9DD2DEFF
PlotHistogramHovered: F4F492FF
TableHeaderBg: 1B282EFF
TableBorderStrong: 4C4C4C80
TableBorderLight: FCEDFC26
TableRowBg: 00000000
TableRowBgAlt: 9DD2DE0D
TextLink: 5C9FD1FF
TextSelectedBg: 1C4449FF
TreeLines: FCEDFC26
DragDropTarget: F8AD34FF
DragDropTargetBg: F8AD3430
UnsavedMarker: F8AD34FF
NavCursor: 5C9FD1FF
NavWindowingHighlight: 9DD2DEB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 5C9FD1FF
accent_alt: FFB7C4FF
error: FF0000FF
error_dim: 772222FF
warning: F8AD34FF
warning_dim: 986032FF
success: 227722FF
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: 5C9FD1FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Everforest Hard
@base default
[colors]
Text: D3C6AAFF
TextDisabled: 9DA9A0FF
WindowBg: 1E2326FF
ChildBg: 272E33FF
PopupBg: 272E33FF
Border: 7A847880
BorderShadow: 00000000
FrameBg: 2E383CFF
FrameBgHovered: 424949FF
FrameBgActive: 525754FF
TitleBg: 272E33FF
TitleBgActive: 383D3FFF
TitleBgCollapsed: 272E33FF
MenuBarBg: 272E33FF
ScrollbarBg: 272E33FF
ScrollbarGrab: 4F5452FF
ScrollbarGrabHovered: 666861FF
ScrollbarGrabActive: 7D7C71FF
CheckMark: E67E80FF
CheckboxSelectedBg: 654D50FF
SliderGrab: E67E80B0
SliderGrabActive: E67E80FF
Button: 2E383CFF
ButtonHovered: 454C4BFF
ButtonActive: 785457FF
Header: 303333FF
HeaderHovered: 3F403EFF
HeaderActive: 644346FF
Separator: 7A847860
SeparatorHovered: 4F5B58FF
SeparatorActive: E67E80FF
ResizeGrip: 7A847860
ResizeGripHovered: E67E80A0
ResizeGripActive: E67E80FF
InputTextCursor: A7C080AA
TabHovered: 484F4EFF
Tab: 272E33FF
TabSelected: 3F4647FF
TabSelectedOverline: E67E80FF
TabDimmed: 1E2326FF
TabDimmedSelected: 272E33FF
TabDimmedSelectedOverline: 7A8478FF
DockingPreview: E67E8070
DockingEmptyBg: 1E2326FF
PlotLines: E67E80FF
PlotLinesHovered: D699B6FF
PlotHistogram: DBBC7FFF
PlotHistogramHovered: D699B6FF
TableHeaderBg: 353A3DFF
TableBorderStrong: 7A847880
TableBorderLight: A7C08022
TableRowBg: 00000000
TableRowBgAlt: D3C6AA0D
TextLink: E67E80FF
TextSelectedBg: 3C4841FF
TreeLines: A7C08022
DragDropTarget: DBBC7FFF
DragDropTargetBg: DBBC7F30
UnsavedMarker: DBBC7FFF
NavCursor: E67E80FF
NavWindowingHighlight: D3C6AAB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: E67E80FF
accent_alt: A7C080FF
error: FF0000FF
error_dim: E67E80FF
warning: DBBC7FFF
warning_dim: 45443CFF
success: 3C4841FF
dim: 9DA9A0FF
neutral: 7A8478FF
active_window_border: E67E80FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Gruber Darker
@base default
[colors]
Text: BFC9DBFF
TextDisabled: 87919DFF
WindowBg: 181818FF
ChildBg: 141416FF
PopupBg: 141416FF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 453D41FF
FrameBgHovered: 544E53FF
FrameBgActive: 605C63FF
TitleBg: 141416FF
TitleBgActive: 25262AFF
TitleBgCollapsed: 141416FF
MenuBarBg: 141416FF
ScrollbarBg: 141416FF
ScrollbarGrab: 5D5960FF
ScrollbarGrabHovered: 6E6D75FF
ScrollbarGrabActive: 80808BFF
CheckMark: FFDD33FF
CheckboxSelectedBg: 7D6D3DFF
SliderGrab: FFDD33B0
SliderGrabActive: FFDD33FF
Button: 453D41FF
ButtonHovered: 565157FF
ButtonActive: 8F7D3BFF
Header: 292A2CFF
HeaderHovered: 36383BFF
HeaderActive: 695D21FF
Separator: 4C4C4C60
SeparatorHovered: 484848FF
SeparatorActive: FFDD33FF
ResizeGrip: 4C4C4C60
ResizeGripHovered: FFDD33A0
ResizeGripActive: FFDD33FF
InputTextCursor: FFDD33FF
TabHovered: 59535AFF
Tab: 141416FF
TabSelected: 514B50FF
TabSelectedOverline: FFDD33FF
TabDimmed: 181818FF
TabDimmedSelected: 141416FF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: FFDD3370
DockingEmptyBg: 181818FF
PlotLines: FFDD33FF
PlotLinesHovered: D89B75FF
PlotHistogram: E4E4EFFF
PlotHistogramHovered: D89B75FF
TableHeaderBg: 222226FF
TableBorderStrong: 4C4C4C80
TableBorderLight: FCEDFC26
TableRowBg: 00000000
TableRowBgAlt: BFC9DB0D
TextLink: FFDD33FF
TextSelectedBg: 484848FF
TreeLines: FCEDFC26
DragDropTarget: CB4B16FF
DragDropTargetBg: CB4B1630
UnsavedMarker: CB4B16FF
NavCursor: FFDD33FF
NavWindowingHighlight: BFC9DBB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: FFDD33FF
accent_alt: E4E4EFFF
error: FF0000FF
error_dim: 772222FF
warning: CB4B16FF
warning_dim: 986032FF
success: 227722FF
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: FFDD33FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Gruvbox Flat
@base default
[colors]
Text: D4BE98FF
TextDisabled: 87919DFF
WindowBg: 32302FFF
ChildBg: 32302FFF
PopupBg: 32302FFF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 5A524CFF
FrameBgHovered: 695F55FF
FrameBgActive: 756A5DFF
TitleBg: 32302FFF
TitleBgActive: 423E3AFF
TitleBgCollapsed: 32302FFF
MenuBarBg: 32302FFF
ScrollbarBg: 32302FFF
ScrollbarGrab: 72685BFF
ScrollbarGrabHovered: 837766FF
ScrollbarGrabActive: 958670FF
CheckMark: D79921FF
CheckboxSelectedBg: 80673FFF
SliderGrab: D79921B0
SliderGrabActive: D79921FF
Button: 5A524CFF
ButtonHovered: 6B6157FF
ButtonActive: 8C6E3BFF
Header: 423E3AFF
HeaderHovered: 4F4A42FF
HeaderActive: 6C552AFF
Separator: 4C4C4C60
SeparatorHovered: 282828FF
SeparatorActive: D79921FF
ResizeGrip: 4C4C4C60
ResizeGripHovered: D79921A0
ResizeGripActive: D79921FF
InputTextCursor: 45403DFF
TabHovered: 6E6358FF
Tab: 32302FFF
TabSelected: 665D54FF
TabSelectedOverline: D79921FF
TabDimmed: 32302FFF
TabDimmedSelected: 32302FFF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: D7992170
DockingEmptyBg: 32302FFF
PlotLines: D79921FF
PlotLinesHovered: 458588FF
PlotHistogram: D3869BFF
PlotHistogramHovered: 458588FF
TableHeaderBg: 3F3B37FF
TableBorderStrong: 4C4C4C80
TableBorderLight: FCEDFC26
TableRowBg: 00000000
TableRowBgAlt: D4BE980D
TextLink: D79921FF
TextSelectedBg: 3C3836FF
TreeLines: FCEDFC26
DragDropTarget: F8AD34FF
DragDropTargetBg: F8AD3430
UnsavedMarker: F8AD34FF
NavCursor: D79921FF
NavWindowingHighlight: D4BE98B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: D79921FF
accent_alt: FB4934FF
error: FF0000FF
error_dim: 772222FF
warning: F8AD34FF
warning_dim: 986032FF
success: 227722FF
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: D79921FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Halogen
@base default
[colors]
Text: BFC9DBFF
TextDisabled: 87919DFF
WindowBg: 181818FF
ChildBg: 181818FF
PopupBg: 181818FF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 383838FF
FrameBgHovered: 48494CFF
FrameBgActive: 56585CFF
TitleBg: 181818FF
TitleBgActive: 292A2CFF
TitleBgCollapsed: 181818FF
MenuBarBg: 181818FF
ScrollbarBg: 181818FF
ScrollbarGrab: 535559FF
ScrollbarGrabHovered: 66696FFF
ScrollbarGrabActive: 797E86FF
CheckMark: 3AA982FF
CheckboxSelectedBg: 395A4EFF
SliderGrab: 3AA982B0
SliderGrabActive: 3AA982FF
Button: 383838FF
ButtonHovered: 4B4C4FFF
ButtonActive: 396556FF
Header: 292A2CFF
HeaderHovered: 36383BFF
HeaderActive: 244B3DFF
Separator: 4C4C4C60
SeparatorHovered: 8296C130
SeparatorActive: 3AA982FF
ResizeGrip: 4C4C4C60
ResizeGripHovered: 3AA982A0
ResizeGripActive: 3AA982FF
InputTextCursor: BFC9DBFF
TabHovered: 4E4F52FF
Tab: 181818FF
TabSelected: 464748FF
TabSelectedOverline: 3AA982FF
TabDimmed: 181818FF
TabDimmedSelected: 181818FF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: 3AA98270
DockingEmptyBg: 181818FF
PlotLines: 3AA982FF
PlotLinesHovered: D89B75FF
PlotHistogram: 3AA982FF
PlotHistogramHovered: D89B75FF
TableHeaderBg: 252628FF
TableBorderStrong: 4C4C4C80
TableBorderLight: FCEDFC26
TableRowBg: 00000000
TableRowBgAlt: BFC9DB0D
TextLink: 3AA982FF
TextSelectedBg: BFC9DB38
TreeLines: FCEDFC26
DragDropTarget: F8AD34FF
DragDropTargetBg: F8AD3430
UnsavedMarker: F8AD34FF
NavCursor: 3AA982FF
NavWindowingHighlight: BFC9DBB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 3AA982FF
accent_alt: 6E8EB8FF
error: FF0000FF
error_dim: 772222FF
warning: F8AD34FF
warning_dim: 986032FF
success: 18181850
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: 3AA982FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Handmade Hero
@base default
[colors]
Text: CDAA7DFF
TextDisabled: 7F7F7FFF
WindowBg: 161616FF
ChildBg: 141414FF
PopupBg: 141414FF
Border: 7F7F7F80
BorderShadow: 00000000
FrameBg: 18262FFF
FrameBgHovered: 2E3638FF
FrameBgActive: 404340FF
TitleBg: 141414FF
TitleBgActive: 27231FFF
TitleBgCollapsed: 141414FF
MenuBarBg: 141414FF
ScrollbarBg: 141414FF
ScrollbarGrab: 3C403FFF
ScrollbarGrabHovered: 56534AFF
ScrollbarGrabActive: 6F6554FF
CheckMark: B8860BFF
CheckboxSelectedBg: 484324FF
SliderGrab: B8860BB0
SliderGrabActive: B8860BFF
Button: 18262FFF
ButtonHovered: 31383AFF
ButtonActive: 584C21FF
Header: 282520FF
HeaderHovered: 373129FF
HeaderActive: 4F3D12FF
Separator: 7F7F7F60
SeparatorHovered: CDAA7D4C
SeparatorActive: B8860BFF
ResizeGrip: 7F7F7F60
ResizeGripHovered: B8860BA0
ResizeGripActive: B8860BFF
InputTextCursor: 40FF40FF
TabHovered: 353B3BFF
Tab: 141414FF
TabSelected: 2A3337FF
TabSelectedOverline: B8860BFF
TabDimmed: 161616FF
TabDimmedSelected: 141414FF
TabDimmedSelectedOverline: 7F7F7FFF
DockingPreview: B8860B70
DockingEmptyBg: 161616FF
PlotLines: B8860BFF
PlotLinesHovered: D89B75FF
PlotHistogram: 6B8E23FF
PlotHistogramHovered: D89B75FF
TableHeaderBg: 23201CFF
TableBorderStrong: 7F7F7F80
TableBorderLight: FCEDFC26
TableRowBg: 00000000
TableRowBgAlt: CDAA7D0D
TextLink: B8860BFF
TextSelectedBg: 0100CDFF
TreeLines: FCEDFC26
DragDropTarget: FFAD34FF
DragDropTargetBg: FFAD3430
UnsavedMarker: FFAD34FF
NavCursor: B8860BFF
NavWindowingHighlight: CDAA7DB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: B8860BFF
accent_alt: CDAA7DFF
error: FF0000FF
error_dim: 772222FF
warning: FFAD34FF
warning_dim: 986032FF
success: 227722FF
dim: 7F7F7FFF
neutral: 7F7F7FFF
active_window_border: B8860BFF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Jblow Nostalgia
@base default
[colors]
Text: BFC9DBFF
TextDisabled: 87919DFF
WindowBg: 292929FF
ChildBg: 1A1A1AFF
PopupBg: 1A1A1AFF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 1A1A1AFF
FrameBgHovered: 2E2F31FF
FrameBgActive: 3E4144FF
TitleBg: 1A1A1AFF
TitleBgActive: 2B2C2DFF
TitleBgCollapsed: 1A1A1AFF
MenuBarBg: 1A1A1AFF
ScrollbarBg: 1A1A1AFF
ScrollbarGrab: 3B3D41FF
ScrollbarGrabHovered: 52565CFF
ScrollbarGrabActive: 696E77FF
CheckMark: FFFFFFFF
CheckboxSelectedBg: 5F5F5FFF
SliderGrab: FFFFFFB0
SliderGrabActive: FFFFFFFF
Button: 1A1A1AFF
ButtonHovered: 313335FF
ButtonActive: 767676FF
Header: 38393BFF
HeaderHovered: 444649FF
HeaderActive: 747474FF
Separator: 4C4C4C60
SeparatorHovered: D0C5A9A8
SeparatorActive: FFFFFFFF
ResizeGrip: 4C4C4C60
ResizeGripHovered: FFFFFFA0
ResizeGripActive: FFFFFFFF
InputTextCursor: 89E2A1FF
TabHovered: 343639FF
Tab: 1A1A1AFF
TabSelected: 2B2C2DFF
TabSelectedOverline: FFFFFFFF
TabDimmed: 292929FF
TabDimmedSelected: 1A1A1AFF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: FFFFFF70
DockingEmptyBg: 292929FF
PlotLines: FFFFFFFF
PlotLinesHovered: D89B75FF
PlotHistogram: 7FFFD4FF
PlotHistogramHovered: D89B75FF
TableHeaderBg: 272829FF
TableBorderStrong: 4C4C4C80
TableBorderLight: FCEDFC26
TableRowBg: 00000000
TableRowBgAlt: BFC9DB0D
TextLink: FFFFFFFF
TextSelectedBg: 0000FFFF
TreeLines: FCEDFC26
DragDropTarget: F8AD34FF
DragDropTargetBg: F8AD3430
UnsavedMarker: F8AD34FF
NavCursor: FFFFFFFF
NavWindowingHighlight: BFC9DBB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: FFFFFFFF
accent_alt: D3B58DFF
error: FF0000FF
error_dim: 772222FF
warning: F8AD34FF
warning_dim: 986032FF
success: 227722FF
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: FFFFFFFF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Jblowtorch
@base default
[colors]
Text: BFC9DBFF
TextDisabled: 87919DFF
WindowBg: 072626FF
ChildBg: 10191FFF
PopupBg: 10191FFF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 18262FFF
FrameBgHovered: 2C3A44FF
FrameBgActive: 3D4A55FF
TitleBg: 10191FFF
TitleBgActive: 222B32FF
TitleBgCollapsed: 10191FFF
MenuBarBg: 10191FFF
ScrollbarBg: 10191FFF
ScrollbarGrab: 394751FF
ScrollbarGrabHovered: 515D69FF
ScrollbarGrabActive: 687482FF
CheckMark: E1E1E1FF
CheckboxSelectedBg: 545E64FF
SliderGrab: E1E1E1B0
SliderGrabActive: E1E1E1FF
Button: 18262FFF
ButtonHovered: 2F3D47FF
ButtonActive: 687176FF
Header: 193638FF
HeaderHovered: 284347FF
HeaderActive: 536767FF
Separator: 4C4C4C60
SeparatorHovered: 1C4449FF
SeparatorActive: E1E1E1FF
ResizeGrip: 4C4C4C60
ResizeGripHovered: E1E1E1A0
ResizeGripActive: E1E1E1FF
InputTextCursor: 26B2B2FF
TabHovered: 33404BFF
Tab: 10191FFF
TabSelected: 293640FF
TabSelectedOverline: E1E1E1FF
TabDimmed: 072626FF
TabDimmedSelected: 10191FFF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: E1E1E170
DockingEmptyBg: 072626FF
PlotLines: E1E1E1FF
PlotLinesHovered: D89B75FF
PlotHistogram: 6BE1CDFF
PlotHistogramHovered: D89B75FF
TableHeaderBg: 1E272EFF
TableBorderStrong: 4C4C4C80
TableBorderLight: FCEDFC26
TableRowBg: 00000000
TableRowBgAlt: BFC9DB0D
TextLink: E1E1E1FF
TextSelectedBg: 1C4449FF
TreeLines: FCEDFC26
DragDropTarget: F8AD34FF
DragDropTargetBg: F8AD3430
UnsavedMarker: F8AD34FF
NavCursor: E1E1E1FF
NavWindowingHighlight: BFC9DBB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: E1E1E1FF
accent_alt: D0C5A9FF
error: FF0000FF
error_dim: 772222FF
warning: F8AD34FF
warning_dim: 986032FF
success: 227722FF
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: E1E1E1FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Jellybeans
@base default
[colors]
Text: A8A8A8FF
TextDisabled: 888888FF
WindowBg: 151515FF
ChildBg: 1C1C1CFF
PopupBg: 1C1C1CFF
Border: 88888880
BorderShadow: 00000000
FrameBg: 262626FF
FrameBgHovered: 363636FF
FrameBgActive: 434343FF
TitleBg: 1C1C1CFF
TitleBgActive: 2A2A2AFF
TitleBgCollapsed: 1C1C1CFF
MenuBarBg: 1C1C1CFF
ScrollbarBg: 1C1C1CFF
ScrollbarGrab: 404040FF
ScrollbarGrabHovered: 525252FF
ScrollbarGrabActive: 646464FF
CheckMark: 8197BFFF
CheckboxSelectedBg: 414854FF
SliderGrab: 8197BFB0
SliderGrabActive: 8197BFFF
Button: 262626FF
ButtonHovered: 383838FF
ButtonActive: 4A5363FF
Header: 242424FF
HeaderHovered: 2F2F2FFF
HeaderActive: 3B4351FF
Separator: 88888860
SeparatorHovered: 3D4070FF
SeparatorActive: 8197BFFF
ResizeGrip: 88888860
ResizeGripHovered: 8197BFA0
ResizeGripActive: 8197BFFF
InputTextCursor: FFFFAAFF
TabHovered: 3B3B3BFF
Tab: 1C1C1CFF
TabSelected: 333333FF
TabSelectedOverline: 8197BFFF
TabDimmed: 151515FF
TabDimmedSelected: 1C1C1CFF
TabDimmedSelectedOverline: 888888FF
DockingPreview: 8197BF70
DockingEmptyBg: 151515FF
PlotLines: 8197BFFF
PlotLinesHovered: 3D4070FF
PlotHistogram: CF6A4CFF
PlotHistogramHovered: 3D4070FF
TableHeaderBg: 272727FF
TableBorderStrong: 88888880
TableBorderLight: 303030FF
TableRowBg: 00000000
TableRowBgAlt: A8A8A80D
TextLink: 8197BFFF
TextSelectedBg: 3D4070FF
TreeLines: 303030FF
DragDropTarget: D8AD4BFF
DragDropTargetBg: D8AD4B30
UnsavedMarker: D8AD4BFF
NavCursor: 8197BFFF
NavWindowingHighlight: A8A8A8B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 8197BFFF
accent_alt: FFC66DFF
error: E84F4FFF
error_dim: E84F4FFF
warning: D8AD4BFF
warning_dim: D8AD4BFF
success: 99AD6AFF
dim: 888888FF
neutral: 888888FF
active_window_border: 8197BFFF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Kanagawa
@base default
[colors]
Text: DCD7BAFF
TextDisabled: C8C093FF
WindowBg: 1F1F28FF
ChildBg: 16161DFF
PopupBg: 16161DFF
Border: C8C09380
BorderShadow: 00000000
FrameBg: 2A2A37FF
FrameBgHovered: 3F3F47FF
FrameBgActive: 515054FF
TitleBg: 16161DFF
TitleBgActive: 2A292DFF
TitleBgCollapsed: 16161DFF
MenuBarBg: 16161DFF
ScrollbarBg: 16161DFF
ScrollbarGrab: 4E4D51FF
ScrollbarGrabHovered: 676564FF
ScrollbarGrabActive: 7F7D76FF
CheckMark: 957FB8FF
CheckboxSelectedBg: 4A445EFF
SliderGrab: 957FB8B0
SliderGrabActive: 957FB8FF
Button: 2A2A37FF
ButtonHovered: 434249FF
ButtonActive: 554C6BFF
Header: 323137FF
HeaderHovered: 414042FF
HeaderActive: 48415AFF
Separator: C8C09360
SeparatorHovered: 2D4F67FF
SeparatorActive: 957FB8FF
ResizeGrip: C8C09360
ResizeGripHovered: 957FB8A0
ResizeGripActive: 957FB8FF
InputTextCursor: C8C093FF
TabHovered: 46464CFF
Tab: 16161DFF
TabSelected: 3C3B44FF
TabSelectedOverline: 957FB8FF
TabDimmed: 1F1F28FF
TabDimmedSelected: 16161DFF
TabDimmedSelectedOverline: C8C093FF
DockingPreview: 957FB870
DockingEmptyBg: 1F1F28FF
PlotLines: 957FB8FF
PlotLinesHovered: 2D4F67FF
PlotHistogram: FFA066FF
PlotHistogramHovered: 2D4F67FF
TableHeaderBg: 26252AFF
TableBorderStrong: C8C09380
TableBorderLight: 363646FF
TableRowBg: 00000000
TableRowBgAlt: DCD7BA0D
TextLink: 957FB8FF
TextSelectedBg: 2D4F67FF
TreeLines: 363646FF
DragDropTarget: FFA066FF
DragDropTargetBg: FFA06630
UnsavedMarker: FFA066FF
NavCursor: 957FB8FF
NavWindowingHighlight: DCD7BAB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 957FB8FF
accent_alt: 7AA89FFF
error: FF5D62FF
error_dim: FF5D62FF
warning: FFA066FF
warning_dim: FFA066FF
success: 98BB6CFF
dim: C8C093FF
neutral: C8C093FF
active_window_border: 957FB8FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Kujukuju
@base default
[colors]
Text: CBCEBCFF
TextDisabled: 87919DFF
WindowBg: 141824FF
ChildBg: 080B14FF
PopupBg: 080B14FF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 12182BFF
FrameBgHovered: 282E3CFF
FrameBgActive: 3B404BFF
TitleBg: 080B14FF
TitleBgActive: 1C1F25FF
TitleBgCollapsed: 080B14FF
MenuBarBg: 080B14FF
ScrollbarBg: 080B14FF
ScrollbarGrab: 373C48FF
ScrollbarGrabHovered: 51565CFF
ScrollbarGrabActive: 6B6F71FF
CheckMark: F07178FF
CheckboxSelectedBg: 553342FF
SliderGrab: F07178B0
SliderGrabActive: F07178FF
Button: 12182BFF
ButtonHovered: 2C313FFF
ButtonActive: 6B3C4AFF
Header: 262A33FF
HeaderHovered: 35393FFF
HeaderActive: 613741FF
Separator: 4C4C4C60
SeparatorHovered: 1C4449FF
SeparatorActive: F07178FF
ResizeGrip: 4C4C4C60
ResizeGripHovered: F07178A0
ResizeGripActive: F07178FF
InputTextCursor: 26B2B2FF
TabHovered: 303542FF
Tab: 080B14FF
TabSelected: 252A3AFF
TabSelectedOverline: F07178FF
TabDimmed: 141824FF
TabDimmedSelected: 080B14FF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: F0717870
DockingEmptyBg: 141824FF
PlotLines: F07178FF
PlotLinesHovered: E4D97DFF
PlotHistogram: FF6A27FF
PlotHistogramHovered: E4D97DFF
TableHeaderBg: 181B21FF
TableBorderStrong: 4C4C4C80
TableBorderLight: FCEDFC26
TableRowBg: 00000000
TableRowBgAlt: CBCEBC0D
TextLink: F07178FF
TextSelectedBg: 1A3C49FF
TreeLines: FCEDFC26
DragDropTarget: F8AD34FF
DragDropTargetBg: F8AD3430
UnsavedMarker: F8AD34FF
NavCursor: F07178FF
NavWindowingHighlight: CBCEBCB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: F07178FF
accent_alt: FFCC66FF
error: FF0000FF
error_dim: 772222FF
warning: F8AD34FF
warning_dim: 986032FF
success: 227722FF
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: F07178FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Nord Midnight
@base default
[colors]
Text: ECEFF4FF
TextDisabled: ECEFF49F
WindowBg: 1D2129FF
ChildBg: 3B4252FF
PopupBg: 3B4252FF
Border: 2E344080
BorderShadow: 00000000
FrameBg: 434C5EFF
FrameBgHovered: 576070FF
FrameBgActive: 68707FFF
TitleBg: 3B4252FF
TitleBgActive: 4D5362FF
TitleBgCollapsed: 3B4252FF
MenuBarBg: 3B4252FF
ScrollbarBg: 3B4252FF
ScrollbarGrab: 656D7CFF
ScrollbarGrabHovered: 7C8391FF
ScrollbarGrabActive: 949AA6FF
CheckMark: 81A1C1FF
CheckboxSelectedBg: 56667CFF
SliderGrab: 81A1C1B0
SliderGrabActive: 81A1C1FF
Button: 434C5EFF
ButtonHovered: 5B6373FF
ButtonActive: 5C6E86FF
Header: 32363DFF
HeaderHovered: 42464EFF
HeaderActive: 404E5EFF
Separator: 2E344060
SeparatorHovered: 3B4252FF
SeparatorActive: 81A1C1FF
ResizeGrip: 2E344060
ResizeGripHovered: 81A1C1A0
ResizeGripActive: 81A1C1FF
InputTextCursor: D8DEE99F
TabHovered: 5E6676FF
Tab: 3B4252FF
TabSelected: 545C6DFF
TabSelectedOverline: 81A1C1FF
TabDimmed: 1D2129FF
TabDimmedSelected: 3B4252FF
TabDimmedSelectedOverline: 2E3440FF
DockingPreview: 81A1C170
DockingEmptyBg: 1D2129FF
PlotLines: 81A1C1FF
PlotLinesHovered: 5E81ACFF
PlotHistogram: B48EADFF
PlotHistogramHovered: 5E81ACFF
TableHeaderBg: 49505FFF
TableBorderStrong: 2E344080
TableBorderLight: 434C5EFF
TableRowBg: 00000000
TableRowBgAlt: ECEFF40D
TextLink: 81A1C1FF
TextSelectedBg: 434C5EFF
TreeLines: 434C5EFF
DragDropTarget: D08770FF
DragDropTargetBg: D0877030
UnsavedMarker: D08770FF
NavCursor: 81A1C1FF
NavWindowingHighlight: ECEFF4B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 81A1C1FF
accent_alt: 88C0D0FF
error: BF616AFF
error_dim: BF616AFF
warning: D08770FF
warning_dim: D08770FF
success: 5E81ACFF
dim: ECEFF49F
neutral: 2E3440FF
active_window_border: 81A1C1FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Nord
@base default
[colors]
Text: ECEFF4FF
TextDisabled: ECEFF49F
WindowBg: 2E3440FF
ChildBg: 3B4252FF
PopupBg: 3B4252FF
Border: 2E344080
BorderShadow: 00000000
FrameBg: 434C5EFF
FrameBgHovered: 576070FF
FrameBgActive: 68707FFF
TitleBg: 3B4252FF
TitleBgActive: 4D5362FF
TitleBgCollapsed: 3B4252FF
MenuBarBg: 3B4252FF
ScrollbarBg: 3B4252FF
ScrollbarGrab: 656D7CFF
ScrollbarGrabHovered: 7C8391FF
ScrollbarGrabActive: 949AA6FF
CheckMark: 81A1C1FF
CheckboxSelectedBg: 56667CFF
SliderGrab: 81A1C1B0
SliderGrabActive: 81A1C1FF
Button: 434C5EFF
ButtonHovered: 5B6373FF
ButtonActive: 5C6E86FF
Header: 414752FF
HeaderHovered: 505660FF
HeaderActive: 4B5A6DFF
Separator: 2E344060
SeparatorHovered: 3B4252FF
SeparatorActive: 81A1C1FF
ResizeGrip: 2E344060
ResizeGripHovered: 81A1C1A0
ResizeGripActive: 81A1C1FF
InputTextCursor: D8DEE99F
TabHovered: 5E6676FF
Tab: 3B4252FF
TabSelected: 545C6DFF
TabSelectedOverline: 81A1C1FF
TabDimmed: 2E3440FF
TabDimmedSelected: 3B4252FF
TabDimmedSelectedOverline: 2E3440FF
DockingPreview: 81A1C170
DockingEmptyBg: 2E3440FF
PlotLines: 81A1C1FF
PlotLinesHovered: 5E81ACFF
PlotHistogram: B48EADFF
PlotHistogramHovered: 5E81ACFF
TableHeaderBg: 49505FFF
TableBorderStrong: 2E344080
TableBorderLight: 434C5EFF
TableRowBg: 00000000
TableRowBgAlt: ECEFF40D
TextLink: 81A1C1FF
TextSelectedBg: 434C5EFF
TreeLines: 434C5EFF
DragDropTarget: D08770FF
DragDropTargetBg: D0877030
UnsavedMarker: D08770FF
NavCursor: 81A1C1FF
NavWindowingHighlight: ECEFF4B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 81A1C1FF
accent_alt: 88C0D0FF
error: BF616AFF
error_dim: BF616A9F
warning: D08770FF
warning_dim: D087709F
success: 5E81ACFF
dim: ECEFF49F
neutral: 2E3440FF
active_window_border: 81A1C1FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Purple
@base default
[colors]
Text: 9DD2DEFF
TextDisabled: 87919DFF
WindowBg: 15212AFF
ChildBg: 10191FFF
PopupBg: 10191FFF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 18262FFF
FrameBgHovered: 283B44FF
FrameBgActive: 354C56FF
TitleBg: 10191FFF
TitleBgActive: 1E2C32FF
TitleBgCollapsed: 10191FFF
MenuBarBg: 10191FFF
ScrollbarBg: 10191FFF
ScrollbarGrab: 334852FF
ScrollbarGrabHovered: 45606BFF
ScrollbarGrabActive: 587983FF
CheckMark: 5C9FD1FF
CheckboxSelectedBg: 2C4A60FF
SliderGrab: 5C9FD1B0
SliderGrabActive: 5C9FD1FF
Button: 18262FFF
ButtonHovered: 2B3E48FF
ButtonActive: 335670FF
Header: 23333CFF
HeaderHovered: 2D414AFF
HeaderActive: 2E4D64FF
Separator: 4C4C4C60
SeparatorHovered: 1C4449FF
SeparatorActive: 5C9FD1FF
ResizeGrip: 4C4C4C60
ResizeGripHovered: 5C9FD1A0
ResizeGripActive: 5C9FD1FF
InputTextCursor: 26B2B2FF
TabHovered: 2D424BFF
Tab: 10191FFF
TabSelected: 253741FF
TabSelectedOverline: 5C9FD1FF
TabDimmed: 15212AFF
TabDimmedSelected: 10191FFF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: 5C9FD170
DockingEmptyBg: 15212AFF
PlotLines: 5C9FD1FF
PlotLinesHovered: F4F492FF
PlotHistogram: 90D0FFFF
PlotHistogramHovered: F4F492FF
TableHeaderBg: 1B282EFF
TableBorderStrong: 4C4C4C80
TableBorderLight: FCEDFC26
TableRowBg: 00000000
TableRowBgAlt: 9DD2DE0D
TextLink: 5C9FD1FF
TextSelectedBg: 1C4449FF
TreeLines: FCEDFC26
DragDropTarget: F8AD34FF
DragDropTargetBg: F8AD3430
UnsavedMarker: F8AD34FF
NavCursor: 5C9FD1FF
NavWindowingHighlight: 9DD2DEB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 5C9FD1FF
accent_alt: 9C7DDBFF
error: FF0000FF
error_dim: 772222FF
warning: F8AD34FF
warning_dim: 986032FF
success: 227722FF
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: 5C9FD1FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Redshift Todo
@base default
[colors]
Text: BFC9DBFF
TextDisabled: 87919DFF
WindowBg: 040408FF
ChildBg: 10191FFF
PopupBg: 10191FFF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 18262FFF
FrameBgHovered: 2C3A44FF
FrameBgActive: 3D4A55FF
TitleBg: 10191FFF
TitleBgActive: 222B32FF
TitleBgCollapsed: 10191FFF
MenuBarBg: 10191FFF
ScrollbarBg: 10191FFF
ScrollbarGrab: 394751FF
ScrollbarGrabHovered: 515D69FF
ScrollbarGrabActive: 687482FF
CheckMark: 8C3485FF
CheckboxSelectedBg: 3B2A49FF
SliderGrab: 8C3485B0
SliderGrabActive: 8C3485FF
Button: 18262FFF
ButtonHovered: 2F3D47FF
ButtonActive: 462C51FF
Header: 17181DFF
HeaderHovered: 26272EFF
HeaderActive: 341534FF
Separator: 4C4C4C60
SeparatorHovered: 491C1CFF
SeparatorActive: 8C3485FF
ResizeGrip: 4C4C4C60
ResizeGripHovered: 8C3485A0
ResizeGripActive: 8C3485FF
InputTextCursor: FF4D40FF
TabHovered: 33404BFF
Tab: 10191FFF
TabSelected: 293640FF
TabSelectedOverline: 8C3485FF
TabDimmed: 040408FF
TabDimmedSelected: 10191FFF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: 8C348570
DockingEmptyBg: 040408FF
PlotLines: 8C3485FF
PlotLinesHovered: F4F492FF
PlotHistogram: DC761DFF
PlotHistogramHovered: F4F492FF
TableHeaderBg: 1E272EFF
TableBorderStrong: 4C4C4C80
TableBorderLight: CF447756
TableRowBg: 00000000
TableRowBgAlt: BFC9DB0D
TextLink: 8C3485FF
TextSelectedBg: 206A7FAF
TreeLines: CF447756
DragDropTarget: F8AD34FF
DragDropTargetBg: F8AD3430
UnsavedMarker: F8AD34FF
NavCursor: 8C3485FF
NavWindowingHighlight: BFC9DBB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 8C3485FF
accent_alt: CB5D7DFF
error: FF0000FF
error_dim: 772222FF
warning: F8AD34FF
warning_dim: 986032FF
success: 227722FF
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: F8AD34FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Redshift
@base default
[colors]
Text: BFC9DBFF
TextDisabled: 87919DFF
WindowBg: 040608FF
ChildBg: 10191FFF
PopupBg: 10191FFF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 18262FFF
FrameBgHovered: 2C3A44FF
FrameBgActive: 3D4A55FF
TitleBg: 10191FFF
TitleBgActive: 222B32FF
TitleBgCollapsed: 10191FFF
MenuBarBg: 10191FFF
ScrollbarBg: 10191FFF
ScrollbarGrab: 394751FF
ScrollbarGrabHovered: 515D69FF
ScrollbarGrabActive: 687482FF
CheckMark: 8C485CFF
CheckboxSelectedBg: 3B303DFF
SliderGrab: 8C485CB0
SliderGrabActive: 8C485CFF
Button: 18262FFF
ButtonHovered: 2F3D47FF
ButtonActive: 463441FF
Header: 171A1DFF
HeaderHovered: 26292EFF
HeaderActive: 341D25FF
Separator: 4C4C4C60
SeparatorHovered: 491C1CFF
SeparatorActive: 8C485CFF
ResizeGrip: 4C4C4C60
ResizeGripHovered: 8C485CA0
ResizeGripActive: 8C485CFF
InputTextCursor: FF4D40FF
TabHovered: 33404BFF
Tab: 10191FFF
TabSelected: 293640FF
TabSelectedOverline: 8C485CFF
TabDimmed: 040608FF
TabDimmedSelected: 10191FFF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: 8C485C70
DockingEmptyBg: 040608FF
PlotLines: 8C485CFF
PlotLinesHovered: F4F492FF
PlotHistogram: 77B7B8FF
PlotHistogramHovered: F4F492FF
TableHeaderBg: 1E272EFF
TableBorderStrong: 4C4C4C80
TableBorderLight: CF447756
TableRowBg: 00000000
TableRowBgAlt: BFC9DB0D
TextLink: 8C485CFF
TextSelectedBg: 206A7FAF
TreeLines: CF447756
DragDropTarget: F8AD34FF
DragDropTargetBg: F8AD3430
UnsavedMarker: F8AD34FF
NavCursor: 8C485CFF
NavWindowingHighlight: BFC9DBB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 8C485CFF
accent_alt: CB5D7DFF
error: FF0000FF
error_dim: 772222FF
warning: F8AD34FF
warning_dim: 986032FF
success: 227722FF
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: F8AD34FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Rose Pine Dawn
@base default
[colors]
Text: 575279FF
TextDisabled: 797593FF
WindowBg: FAF4EDFF
ChildBg: FFFAF3FF
PopupBg: FFFAF3FF
Border: 79759380
BorderShadow: 00000000
FrameBg: F2E9E1FF
FrameBgHovered: DFD7D5FF
FrameBgActive: D0C8CAFF
TitleBg: FFFAF3FF
TitleBgActive: EEE9E7FF
TitleBgCollapsed: FFFAF3FF
MenuBarBg: FFFAF3FF
ScrollbarBg: FFFAF3FF
ScrollbarGrab: D3CBCCFF
ScrollbarGrabHovered: BDB6BEFF
ScrollbarGrabActive: A8A1AFFF
CheckMark: 286983FF
CheckboxSelectedBg: B5C3C5FF
SliderGrab: 286983B0
SliderGrabActive: 286983FF
Button: F2E9E1FF
ButtonHovered: DCD4D2FF
ButtonActive: A1B6BBFF
Header: EAE4E1FF
HeaderHovered: DDD7D8FF
HeaderActive: B1C3C8FF
Separator: 79759360
SeparatorHovered: 9893A5FF
SeparatorActive: 286983FF
ResizeGrip: 79759360
ResizeGripHovered: 286983A0
ResizeGripActive: 286983FF
InputTextCursor: 797593FF
TabHovered: D9D1D0FF
Tab: FFFAF3FF
TabSelected: E3DAD7FF
TabSelectedOverline: 286983FF
TabDimmed: FAF4EDFF
TabDimmedSelected: FFFAF3FF
TabDimmedSelectedOverline: 797593FF
DockingPreview: 28698370
DockingEmptyBg: FAF4EDFF
PlotLines: 286983FF
PlotLinesHovered: 9893A5FF
PlotHistogram: EA9D34FF
PlotHistogramHovered: 9893A5FF
TableHeaderBg: F2EDE9FF
TableBorderStrong: 79759380
TableBorderLight: F2E9E1FF
TableRowBg: 00000000
TableRowBgAlt: 5752790D
TextLink: 286983FF
TextSelectedBg: 9893A5FF
TreeLines: F2E9E1FF
DragDropTarget: EA9D34FF
DragDropTargetBg: EA9D3430
UnsavedMarker: EA9D34FF
NavCursor: 286983FF
NavWindowingHighlight: 575279B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 286983FF
accent_alt: 907AA9FF
error: D7827EFF
error_dim: D7827EFF
warning: EA9D34FF
warning_dim: EA9D34FF
success: 56949FFF
dim: 797593FF
neutral: 797593FF
active_window_border: 286983FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Rose Pine Moon
@base default
[colors]
Text: E0DEF4FF
TextDisabled: 908CABFF
WindowBg: 232136FF
ChildBg: 2A273FFF
PopupBg: 2A273FFF
Border: 908CAB80
BorderShadow: 00000000
FrameBg: 393552FF
FrameBgHovered: 4D4965FF
FrameBgActive: 5E5A76FF
TitleBg: 2A273FFF
TitleBgActive: 3C3951FF
TitleBgCollapsed: 2A273FFF
MenuBarBg: 2A273FFF
ScrollbarBg: 2A273FFF
ScrollbarGrab: 5A5772FF
ScrollbarGrabHovered: 726E89FF
ScrollbarGrabActive: 8986A0FF
CheckMark: 3E8FB0FF
CheckboxSelectedBg: 3B506EFF
SliderGrab: 3E8FB0B0
SliderGrabActive: 3E8FB0FF
Button: 393552FF
ButtonHovered: 504D69FF
ButtonActive: 3B5978FF
Header: 363449FF
HeaderHovered: 454358FF
HeaderActive: 2C4861FF
Separator: 908CAB60
SeparatorHovered: 44415AFF
SeparatorActive: 3E8FB0FF
ResizeGrip: 908CAB60
ResizeGripHovered: 3E8FB0A0
ResizeGripActive: 3E8FB0FF
InputTextCursor: 56526EFF
TabHovered: 54506CFF
Tab: 2A273FFF
TabSelected: 4A4662FF
TabSelectedOverline: 3E8FB0FF
TabDimmed: 232136FF
TabDimmedSelected: 2A273FFF
TabDimmedSelectedOverline: 908CABFF
DockingPreview: 3E8FB070
DockingEmptyBg: 232136FF
PlotLines: 3E8FB0FF
PlotLinesHovered: 44415AFF
PlotHistogram: F6C177FF
PlotHistogramHovered: 44415AFF
TableHeaderBg: 39364DFF
TableBorderStrong: 908CAB80
TableBorderLight: 393552FF
TableRowBg: 00000000
TableRowBgAlt: E0DEF40D
TextLink: 3E8FB0FF
TextSelectedBg: 44415AFF
TreeLines: 393552FF
DragDropTarget: F6C177FF
DragDropTargetBg: F6C17730
UnsavedMarker: F6C177FF
NavCursor: 3E8FB0FF
NavWindowingHighlight: E0DEF4B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 3E8FB0FF
accent_alt: C4A7E7FF
error: EB6F92FF
error_dim: EB6F92FF
warning: F6C177FF
warning_dim: F6C177FF
success: 9CCFD8FF
dim: 908CABFF
neutral: 908CABFF
active_window_border: F6C177FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Rose Pine
@base default
[colors]
Text: E0DEF4FF
TextDisabled: 908CABFF
WindowBg: 191724FF
ChildBg: 1F1D2EFF
PopupBg: 1F1D2EFF
Border: 908CAB80
BorderShadow: 00000000
FrameBg: 26233AFF
FrameBgHovered: 3C3950FF
FrameBgActive: 4F4C63FF
TitleBg: 1F1D2EFF
TitleBgActive: 323042FF
TitleBgCollapsed: 1F1D2EFF
MenuBarBg: 1F1D2EFF
ScrollbarBg: 1F1D2EFF
ScrollbarGrab: 4B485FFF
ScrollbarGrabHovered: 656379FF
ScrollbarGrabActive: 7F7D93FF
CheckMark: 31748FFF
CheckboxSelectedBg: 293B54FF
SliderGrab: 31748FB0
SliderGrabActive: 31748FFF
Button: 26233AFF
ButtonHovered: 403D54FF
ButtonActive: 2A435CFF
Header: 2D2B39FF
HeaderHovered: 3D3B49FF
HeaderActive: 213849FF
Separator: 908CAB60
SeparatorHovered: 403D52FF
SeparatorActive: 31748FFF
ResizeGrip: 908CAB60
ResizeGripHovered: 31748FA0
ResizeGripActive: 31748FFF
InputTextCursor: 524F67FF
TabHovered: 444158FF
Tab: 1F1D2EFF
TabSelected: 39364DFF
TabSelectedOverline: 31748FFF
TabDimmed: 191724FF
TabDimmedSelected: 1F1D2EFF
TabDimmedSelectedOverline: 908CABFF
DockingPreview: 31748F70
DockingEmptyBg: 191724FF
PlotLines: 31748FFF
PlotLinesHovered: 403D52FF
PlotHistogram: F6C177FF
PlotHistogramHovered: 403D52FF
TableHeaderBg: 2E2C3EFF
TableBorderStrong: 908CAB80
TableBorderLight: 26233AFF
TableRowBg: 00000000
TableRowBgAlt: E0DEF40D
TextLink: 31748FFF
TextSelectedBg: 403D52FF
TreeLines: 26233AFF
DragDropTarget: F6C177FF
DragDropTargetBg: F6C17730
UnsavedMarker: F6C177FF
NavCursor: 31748FFF
NavWindowingHighlight: E0DEF4B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 31748FFF
accent_alt: C4A7E7FF
error: EB6F92FF
error_dim: EB6F92FF
warning: F6C177FF
warning_dim: F6C177FF
success: 9CCFD8FF
dim: 908CABFF
neutral: 908CABFF
active_window_border: F6C177FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Solarized Dark
@base default
[colors]
Text: BFC9DBFF
TextDisabled: 87919DFF
WindowBg: 002B36FF
ChildBg: 002B36FF
PopupBg: 002B36FF
Border: 07364280
BorderShadow: 00000000
FrameBg: 073642FF
FrameBgHovered: 1D4854FF
FrameBgActive: 2F5664FF
TitleBg: 002B36FF
TitleBgActive: 133B47FF
TitleBgCollapsed: 002B36FF
MenuBarBg: 002B36FF
ScrollbarBg: 002B36FF
ScrollbarGrab: 2C5361FF
ScrollbarGrabHovered: 466876FF
ScrollbarGrabActive: 5F7D8BFF
CheckMark: 859900FF
CheckboxSelectedBg: 2D542EFF
SliderGrab: 859900B0
SliderGrabActive: 859900FF
Button: 073642FF
ButtonHovered: 214B57FF
ButtonActive: 395E28FF
Header: 133B47FF
HeaderHovered: 224754FF
HeaderActive: 2F5223FF
Separator: 07364260
SeparatorHovered: 1C4449FF
SeparatorActive: 859900FF
ResizeGrip: 07364260
ResizeGripHovered: 859900A0
ResizeGripActive: 859900FF
InputTextCursor: 93A1A1FF
TabHovered: 244E5AFF
Tab: 002B36FF
TabSelected: 194551FF
TabSelectedOverline: 859900FF
TabDimmed: 002B36FF
TabDimmedSelected: 002B36FF
TabDimmedSelectedOverline: 073642FF
DockingPreview: 85990070
DockingEmptyBg: 002B36FF
PlotLines: 859900FF
PlotLinesHovered: D89B75FF
PlotHistogram: D33682FF
PlotHistogramHovered: D89B75FF
TableHeaderBg: 0F3843FF
TableBorderStrong: 07364280
TableBorderLight: 93A1A1FF
TableRowBg: 00000000
TableRowBgAlt: BFC9DB0D
TextLink: 859900FF
TextSelectedBg: 073642FF
TreeLines: 93A1A1FF
DragDropTarget: CB4B16FF
DragDropTargetBg: CB4B1630
UnsavedMarker: CB4B16FF
NavCursor: 859900FF
NavWindowingHighlight: BFC9DBB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 859900FF
accent_alt: 839496FF
error: DC322FFF
error_dim: FC32FFFF
warning: CB4B16FF
warning_dim: CB4B16FF
success: 073642FF
dim: 87919DFF
neutral: 073642FF
active_window_border: 859900FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Spacemacs Light
@base default
[colors]
Text: 6C3163FF
TextDisabled: 655370FF
WindowBg: FBF8EFFF
ChildBg: D2CEDAFF
PopupBg: D2CEDAFF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: E3DEDDFF
FrameBgHovered: D5C9CEFF
FrameBgActive: C9B8C2FF
TitleBg: D2CEDAFF
TitleBgActive: C8BECEFF
TitleBgCollapsed: D2CEDAFF
MenuBarBg: D2CEDAFF
ScrollbarBg: D2CEDAFF
ScrollbarGrab: CBBBC5FF
ScrollbarGrabHovered: BBA3B4FF
ScrollbarGrabActive: AA8BA2FF
CheckMark: 3A81C3FF
CheckboxSelectedBg: B0C2D5FF
SliderGrab: 3A81C3B0
SliderGrabActive: 3A81C3FF
Button: E3DEDDFF
ButtonHovered: D2C6CCFF
ButtonActive: 9FB9D3FF
Header: EDE4E1FF
HeaderHovered: E1D4D6FF
HeaderActive: B7CEE0FF
Separator: 4C4C4C60
SeparatorHovered: EFEAE9FF
SeparatorActive: 3A81C3FF
ResizeGrip: 4C4C4C60
ResizeGripHovered: 3A81C3A0
ResizeGripActive: 3A81C3FF
InputTextCursor: 100A14FF
TabHovered: D0C2C9FF
Tab: D2CEDAFF
TabSelected: D7CDD1FF
TabSelectedOverline: 3A81C3FF
TabDimmed: FBF8EFFF
TabDimmedSelected: D2CEDAFF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: 3A81C370
DockingEmptyBg: FBF8EFFF
PlotLines: 3A81C3FF
PlotLinesHovered: BA2F59FF
PlotHistogram: 4E3163FF
PlotHistogramHovered: BA2F59FF
TableHeaderBg: CAC1D0FF
TableBorderStrong: 4C4C4C80
TableBorderLight: DAE6D0FF
TableRowBg: 00000000
TableRowBgAlt: 6C31630D
TextLink: 3A81C3FF
TextSelectedBg: D3D3E7FF
TreeLines: DAE6D0FF
DragDropTarget: DC752FFF
DragDropTargetBg: DC752F30
UnsavedMarker: DC752FFF
NavCursor: 3A81C3FF
NavWindowingHighlight: 6C3163B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 3A81C3FF
accent_alt: 6C3163FF
error: FF0000FF
error_dim: E0211DFF
warning: DC752FFF
warning_dim: DC752F5F
success: 42AE2CFF
dim: 655370FF
neutral: 4C4C4CFF
active_window_border: 6C3163FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Steel Breeze
@base default
[colors]
Text: FFFFFFE0
TextDisabled: FFFFFF80
WindowBg: 00000060
ChildBg: 000000FF
PopupBg: 000000FF
Border: FFFFFF80
BorderShadow: 00000000
FrameBg: FF0000FF
FrameBgHovered: FF1F1FFF
FrameBgActive: FF3838FF
TitleBg: 000000FF
TitleBgActive: 1A1A1AFF
TitleBgCollapsed: 000000FF
MenuBarBg: 000000FF
ScrollbarBg: 000000FF
ScrollbarGrab: FF3333FF
ScrollbarGrabHovered: FF5757FF
ScrollbarGrabActive: FF7A7AFF
CheckMark: 8860FFE0
CheckboxSelectedBg: DB1D4DFF
SliderGrab: 8860FFB0
SliderGrabActive: 8860FFE0
Button: FF0000FF
ButtonHovered: FF2424FF
ButtonActive: CF2666FF
Header: 1A1A1A60
HeaderHovered: 2E2E2E60
HeaderActive: 30225960
Separator: FFFFFF60
SeparatorHovered: FFFFFF40
SeparatorActive: 8860FFE0
ResizeGrip: FFFFFF60
ResizeGripHovered: 8860FFA0
ResizeGripActive: 8860FFE0
InputTextCursor: 4020FFFF
TabHovered: FF2929FF
Tab: 000000FF
TabSelected: FF1A1AFF
TabSelectedOverline: 8860FFE0
TabDimmed: 00000060
TabDimmedSelected: 000000FF
TabDimmedSelectedOverline: FFFFFF40
DockingPreview: 8860FF70
DockingEmptyBg: 00000060
PlotLines: 8860FFE0
PlotLinesHovered: D89B75E0
PlotHistogram: FF6060E0
PlotHistogramHovered: D89B75E0
TableHeaderBg: 141414FF
TableBorderStrong: FFFFFF80
TableBorderLight: FFFFFF20
TableRowBg: 00000000
TableRowBgAlt: FFFFFF0D
TextLink: 8860FFE0
TextSelectedBg: 4000FF80
TreeLines: FFFFFF20
DragDropTarget: FFC044E0
DragDropTargetBg: FFC04430
UnsavedMarker: FFC044E0
NavCursor: 8860FFE0
NavWindowingHighlight: FFFFFFB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 8860FFE0
accent_alt: 0060FFE0
error: EE4444FF
error_dim: EE4444E0
warning: FFC044E0
warning_dim: FFC04480
success: 44C044E0
dim: FFFFFF80
neutral: FFFFFF40
active_window_border: 8860FFFF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Todo Highlight 1
@base default
[colors]
Text: BFC9DBFF
TextDisabled: 87919DFF
WindowBg: 15212AFF
ChildBg: 10191FFF
PopupBg: 10191FFF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 18262FFF
FrameBgHovered: 2C3A44FF
FrameBgActive: 3D4A55FF
TitleBg: 10191FFF
TitleBgActive: 222B32FF
TitleBgCollapsed: 10191FFF
MenuBarBg: 10191FFF
ScrollbarBg: 10191FFF
ScrollbarGrab: 394751FF
ScrollbarGrabHovered: 515D69FF
ScrollbarGrabActive: 687482FF
CheckMark: E0415EFF
CheckboxSelectedBg: 542E3DFF
SliderGrab: E0415EB0
SliderGrabActive: E0415EFF
Button: 18262FFF
ButtonHovered: 2F3D47FF
ButtonActive: 683142FF
Header: 26323CFF
HeaderHovered: 343F4AFF
HeaderActive: 5C2C3CFF
Separator: 4C4C4C60
SeparatorHovered: 1C4449FF
SeparatorActive: E0415EFF
ResizeGrip: 4C4C4C60
ResizeGripHovered: E0415EA0
ResizeGripActive: E0415EFF
InputTextCursor: 52F9FFFF
TabHovered: 33404BFF
Tab: 10191FFF
TabSelected: 293640FF
TabSelectedOverline: E0415EFF
TabDimmed: 15212AFF
TabDimmedSelected: 10191FFF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: E0415E70
DockingEmptyBg: 15212AFF
PlotLines: E0415EFF
PlotLinesHovered: D89B75FF
PlotHistogram: FC4E03FF
PlotHistogramHovered: D89B75FF
TableHeaderBg: 1E272EFF
TableBorderStrong: 4C4C4C80
TableBorderLight: FCEDFC26
TableRowBg: 00000000
TableRowBgAlt: BFC9DB0D
TextLink: E0415EFF
TextSelectedBg: 1C4449FF
TreeLines: FCEDFC26
DragDropTarget: F8AD34FF
DragDropTargetBg: F8AD3430
UnsavedMarker: F8AD34FF
NavCursor: E0415EFF
NavWindowingHighlight: BFC9DBB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: E0415EFF
accent_alt: 8398D5FF
error: FF0000FF
error_dim: 772222FF
warning: F8AD34FF
warning_dim: 986032FF
success: 227722FF
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: F8AD34FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Tokyo Night Light Todo
@base default
[colors]
Text: 343B58FF
TextDisabled: 9DA0ABFF
WindowBg: D5D6DBFF
ChildBg: C1C2C7FF
PopupBg: C1C2C7FF
Border: 9DA0AB80
BorderShadow: 00000000
FrameBg: C1C2C7FF
FrameBgHovered: B0B2BAFF
FrameBgActive: A2A4AFFF
TitleBg: C1C2C7FF
TitleBgActive: B3B5BCFF
TitleBgCollapsed: C1C2C7FF
MenuBarBg: C1C2C7FF
ScrollbarBg: C1C2C7FF
ScrollbarGrab: A5A7B1FF
ScrollbarGrabHovered: 9194A1FF
ScrollbarGrabActive: 7D8192FF
CheckMark: 7ABB74FF
CheckboxSelectedBg: ACC0AEFF
SliderGrab: 7ABB74B0
SliderGrabActive: 7ABB74FF
Button: C1C2C7FF
ButtonHovered: ADAFB7FF
ButtonActive: A5BFA6FF
Header: C5C7CEFF
HeaderHovered: B8BAC3FF
HeaderActive: B5CDB7FF
Separator: 9DA0AB60
SeparatorHovered: C1C2C7FF
SeparatorActive: 7ABB74FF
ResizeGrip: 9DA0AB60
ResizeGripHovered: 7ABB74A0
ResizeGripActive: 7ABB74FF
InputTextCursor: 4C505EFF
TabHovered: AAACB5FF
Tab: C1C2C7FF
TabSelected: B3B5BCFF
TabSelectedOverline: 7ABB74FF
TabDimmed: D5D6DBFF
TabDimmedSelected: C1C2C7FF
TabDimmedSelectedOverline: 9DA0ABFF
DockingPreview: 7ABB7470
DockingEmptyBg: D5D6DBFF
PlotLines: 7ABB74FF
PlotLinesHovered: D89B75FF
PlotHistogram: 965027FF
PlotHistogramHovered: D89B75FF
TableHeaderBg: B6B7BEFF
TableBorderStrong: 9DA0AB80
TableBorderLight: CFCFE4FF
TableRowBg: 00000000
TableRowBgAlt: 343B580D
TextLink: 7ABB74FF
TextSelectedBg: C8C9DEFF
TreeLines: CFCFE4FF
DragDropTarget: D28822FF
DragDropTargetBg: D2882230
UnsavedMarker: D28822FF
NavCursor: 7ABB74FF
NavWindowingHighlight: 343B58B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 7ABB74FF
accent_alt: 34548AFF
error: DD4E52FF
error_dim: BD4040FF
warning: D28822FF
warning_dim: D28822FF
success: 9ECE6AFF
dim: 9DA0ABFF
neutral: 9DA0ABFF
active_window_border: 34548AFF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Tokyo Night Light
@base default
[colors]
Text: 343B58FF
TextDisabled: 9DA0ABFF
WindowBg: D5D6DBFF
ChildBg: C1C2C7FF
PopupBg: C1C2C7FF
Border: 9DA0AB80
BorderShadow: 00000000
FrameBg: C1C2C7FF
FrameBgHovered: B0B2BAFF
FrameBgActive: A2A4AFFF
TitleBg: C1C2C7FF
TitleBgActive: B3B5BCFF
TitleBgCollapsed: C1C2C7FF
MenuBarBg: C1C2C7FF
ScrollbarBg: C1C2C7FF
ScrollbarGrab: A5A7B1FF
ScrollbarGrabHovered: 9194A1FF
ScrollbarGrabActive: 7D8192FF
CheckMark: 5A4A78FF
CheckboxSelectedBg: A29EAFFF
SliderGrab: 5A4A78B0
SliderGrabActive: 5A4A78FF
Button: C1C2C7FF
ButtonHovered: ADAFB7FF
ButtonActive: 9892A7FF
Header: C5C7CEFF
HeaderHovered: B8BAC3FF
HeaderActive: AAA5B8FF
Separator: 9DA0AB60
SeparatorHovered: C1C2C7FF
SeparatorActive: 5A4A78FF
ResizeGrip: 9DA0AB60
ResizeGripHovered: 5A4A78A0
ResizeGripActive: 5A4A78FF
InputTextCursor: 4C505EFF
TabHovered: AAACB5FF
Tab: C1C2C7FF
TabSelected: B3B5BCFF
TabSelectedOverline: 5A4A78FF
TabDimmed: D5D6DBFF
TabDimmedSelected: C1C2C7FF
TabDimmedSelectedOverline: 9DA0ABFF
DockingPreview: 5A4A7870
DockingEmptyBg: D5D6DBFF
PlotLines: 5A4A78FF
PlotLinesHovered: D89B75FF
PlotHistogram: 965027FF
PlotHistogramHovered: D89B75FF
TableHeaderBg: B6B7BEFF
TableBorderStrong: 9DA0AB80
TableBorderLight: CFCFE4FF
TableRowBg: 00000000
TableRowBgAlt: 343B580D
TextLink: 5A4A78FF
TextSelectedBg: C8C9DEFF
TreeLines: CFCFE4FF
DragDropTarget: D28822FF
DragDropTargetBg: D2882230
UnsavedMarker: D28822FF
NavCursor: 5A4A78FF
NavWindowingHighlight: 343B58B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 5A4A78FF
accent_alt: 34548AFF
error: DD4E52FF
error_dim: BD4040FF
warning: D28822FF
warning_dim: D28822FF
success: 9ECE6AFF
dim: 9DA0ABFF
neutral: 9DA0ABFF
active_window_border: 5A4A78FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Tokyo Night Todo
@base default
[colors]
Text: A9B1D6FF
TextDisabled: 363B54FF
WindowBg: 1A1B26FF
ChildBg: 24283BFF
PopupBg: 24283BFF
Border: 363B5480
BorderShadow: 00000000
FrameBg: 24283BFF
FrameBgHovered: 34384EFF
FrameBgActive: 41465DFF
TitleBg: 24283BFF
TitleBgActive: 31364BFF
TitleBgCollapsed: 24283BFF
MenuBarBg: 24283BFF
ScrollbarBg: 24283BFF
ScrollbarGrab: 3F435AFF
ScrollbarGrabHovered: 515770FF
ScrollbarGrabActive: 646A85FF
CheckMark: 7B3CEFFF
CheckboxSelectedBg: 3E2E71FF
SliderGrab: 7B3CEFB0
SliderGrabActive: 7B3CEFFF
Button: 24283BFF
ButtonHovered: 373B51FF
ButtonActive: 473083FF
Header: 282A38FF
HeaderHovered: 343646FF
HeaderActive: 3C276CFF
Separator: 363B5460
SeparatorHovered: 101014FF
SeparatorActive: 7B3CEFFF
ResizeGrip: 363B5460
ResizeGripHovered: 7B3CEFA0
ResizeGripActive: 7B3CEFFF
InputTextCursor: C0CAF5FF
TabHovered: 393E54FF
Tab: 24283BFF
TabSelected: 31364BFF
TabSelectedOverline: 7B3CEFFF
TabDimmed: 1A1B26FF
TabDimmedSelected: 24283BFF
TabDimmedSelectedOverline: 363B54FF
DockingPreview: 7B3CEF70
DockingEmptyBg: 1A1B26FF
PlotLines: 7B3CEFFF
PlotLinesHovered: D89B75FF
PlotHistogram: FF9E64FF
PlotHistogramHovered: D89B75FF
TableHeaderBg: 2F3347FF
TableBorderStrong: 363B5480
TableBorderLight: 2A2F41FF
TableRowBg: 00000000
TableRowBgAlt: A9B1D60D
TextLink: 7B3CEFFF
TextSelectedBg: 2A2F41FF
TreeLines: 2A2F41FF
DragDropTarget: FF9E64FF
DragDropTargetBg: FF9E6430
UnsavedMarker: FF9E64FF
NavCursor: 7B3CEFFF
NavWindowingHighlight: A9B1D6B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 7B3CEFFF
accent_alt: 7AA2F7FF
error: F7768EFF
error_dim: A95263FF
warning: FF9E64FF
warning_dim: 895738FF
success: 485E30FF
dim: 363B54FF
neutral: 363B54FF
active_window_border: FF9E64FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Tokyo Night
@base default
[colors]
Text: A9B1D6FF
TextDisabled: 363B54FF
WindowBg: 1A1B26FF
ChildBg: 24283BFF
PopupBg: 24283BFF
Border: 363B5480
BorderShadow: 00000000
FrameBg: 24283BFF
FrameBgHovered: 34384EFF
FrameBgActive: 41465DFF
TitleBg: 24283BFF
TitleBgActive: 31364BFF
TitleBgCollapsed: 24283BFF
MenuBarBg: 24283BFF
ScrollbarBg: 24283BFF
ScrollbarGrab: 3F435AFF
ScrollbarGrabHovered: 515770FF
ScrollbarGrabActive: 646A85FF
CheckMark: BB9AF7FF
CheckboxSelectedBg: 514A73FF
SliderGrab: BB9AF7B0
SliderGrabActive: BB9AF7FF
Button: 24283BFF
ButtonHovered: 373B51FF
ButtonActive: 605686FF
Header: 282A38FF
HeaderHovered: 343646FF
HeaderActive: 52476FFF
Separator: 363B5460
SeparatorHovered: 101014FF
SeparatorActive: BB9AF7FF
ResizeGrip: 363B5460
ResizeGripHovered: BB9AF7A0
ResizeGripActive: BB9AF7FF
InputTextCursor: C0CAF5FF
TabHovered: 393E54FF
Tab: 24283BFF
TabSelected: 31364BFF
TabSelectedOverline: BB9AF7FF
TabDimmed: 1A1B26FF
TabDimmedSelected: 24283BFF
TabDimmedSelectedOverline: 363B54FF
DockingPreview: BB9AF770
DockingEmptyBg: 1A1B26FF
PlotLines: BB9AF7FF
PlotLinesHovered: D89B75FF
PlotHistogram: FF9E64FF
PlotHistogramHovered: D89B75FF
TableHeaderBg: 2F3347FF
TableBorderStrong: 363B5480
TableBorderLight: 2A2F41FF
TableRowBg: 00000000
TableRowBgAlt: A9B1D60D
TextLink: BB9AF7FF
TextSelectedBg: 2A2F41FF
TreeLines: 2A2F41FF
DragDropTarget: FF9E64FF
DragDropTargetBg: FF9E6430
UnsavedMarker: FF9E64FF
NavCursor: BB9AF7FF
NavWindowingHighlight: A9B1D6B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: BB9AF7FF
accent_alt: 7AA2F7FF
error: F7768EFF
error_dim: A95263FF
warning: FF9E64FF
warning_dim: 895738FF
success: 485E30FF
dim: 363B54FF
neutral: 363B54FF
active_window_border: BB9AF7FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Vscode Dark
@base default
[colors]
Text: D4D4D4FF
TextDisabled: CCCCCCFF
WindowBg: 1E1E1EFF
ChildBg: 252526FF
PopupBg: 252526FF
Border: CCCCCC80
BorderShadow: 00000000
FrameBg: 333333FF
FrameBgHovered: 464646FF
FrameBgActive: 565656FF
TitleBg: 252526FF
TitleBgActive: 373737FF
TitleBgCollapsed: 252526FF
MenuBarBg: 252526FF
ScrollbarBg: 252526FF
ScrollbarGrab: 535353FF
ScrollbarGrabHovered: 6A6A6AFF
ScrollbarGrabActive: 808080FF
CheckMark: 569CD6FF
CheckboxSelectedBg: 3E5364FF
SliderGrab: 569CD6B0
SliderGrabActive: 569CD6FF
Button: 333333FF
ButtonHovered: 4A4A4AFF
ButtonActive: 415D74FF
Header: 303030FF
HeaderHovered: 3F3F3FFF
HeaderActive: 324A5EFF
Separator: CCCCCC60
SeparatorHovered: 3A3D41FF
SeparatorActive: 569CD6FF
ResizeGrip: CCCCCC60
ResizeGripHovered: 569CD6A0
ResizeGripActive: 569CD6FF
InputTextCursor: AEAFADFF
TabHovered: 4D4D4DFF
Tab: 252526FF
TabSelected: 434343FF
TabSelectedOverline: 569CD6FF
TabDimmed: 1E1E1EFF
TabDimmedSelected: 252526FF
TabDimmedSelectedOverline: CCCCCCFF
DockingPreview: 569CD670
DockingEmptyBg: 1E1E1EFF
PlotLines: 569CD6FF
PlotLinesHovered: 3A3D41FF
PlotHistogram: B5CEA8FF
PlotHistogramHovered: 3A3D41FF
TableHeaderBg: 333334FF
TableBorderStrong: CCCCCC80
TableBorderLight: 3C3C3CFF
TableRowBg: 00000000
TableRowBgAlt: D4D4D40D
TextLink: 569CD6FF
TextSelectedBg: 264F78FF
TreeLines: 3C3C3CFF
DragDropTarget: DCDCAAFF
DragDropTargetBg: DCDCAA30
UnsavedMarker: DCDCAAFF
NavCursor: 569CD6FF
NavWindowingHighlight: D4D4D4B0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: 569CD6FF
accent_alt: DCDCAAFF
error: F48771FF
error_dim: F48771FF
warning: DCDCAAFF
warning_dim: DCDCAAFF
success: 89D185FF
dim: CCCCCCFF
neutral: CCCCCCFF
active_window_border: 569CD6FF

View File

@ -0,0 +1,83 @@
# ImGui theme, generated from a focus theme.
# Sizes are inherited from @base; edit freely, this file is not regenerated in place.
@version 1
@name Zenburn
@base default
[colors]
Text: BFC9DBFF
TextDisabled: 87919DFF
WindowBg: 3F3F3FFF
ChildBg: 282828FF
PopupBg: 282828FF
Border: 4C4C4C80
BorderShadow: 00000000
FrameBg: 2E3330FF
FrameBgHovered: 3F4545FF
FrameBgActive: 4E5456FF
TitleBg: 282828FF
TitleBgActive: 37383AFF
TitleBgCollapsed: 282828FF
MenuBarBg: 282828FF
ScrollbarBg: 282828FF
ScrollbarGrab: 4B5152FF
ScrollbarGrabHovered: 5F666AFF
ScrollbarGrabActive: 747B82FF
CheckMark: F0DFAFFF
CheckboxSelectedBg: 686756FF
SliderGrab: F0DFAFB0
SliderGrabActive: F0DFAFFF
Button: 2E3330FF
ButtonHovered: 424848FF
ButtonActive: 7C7863FF
Header: 4C4D4FFF
HeaderHovered: 56585BFF
HeaderActive: 7D7766FF
Separator: 4C4C4C60
SeparatorHovered: 2E3330FF
SeparatorActive: F0DFAFFF
ResizeGrip: 4C4C4C60
ResizeGripHovered: F0DFAFA0
ResizeGripActive: F0DFAFFF
InputTextCursor: 8FAF9FFF
TabHovered: 454B4BFF
Tab: 282828FF
TabSelected: 3D4241FF
TabSelectedOverline: F0DFAFFF
TabDimmed: 3F3F3FFF
TabDimmedSelected: 282828FF
TabDimmedSelectedOverline: 4C4C4CFF
DockingPreview: F0DFAF70
DockingEmptyBg: 3F3F3FFF
PlotLines: F0DFAFFF
PlotLinesHovered: D89B75FF
PlotHistogram: 8CD0D3FF
PlotHistogramHovered: D89B75FF
TableHeaderBg: 343536FF
TableBorderStrong: 4C4C4C80
TableBorderLight: FCEDFC26
TableRowBg: 00000000
TableRowBgAlt: BFC9DB0D
TextLink: F0DFAFFF
TextSelectedBg: 304A3DFF
TreeLines: FCEDFC26
DragDropTarget: F8AD34FF
DragDropTargetBg: F8AD3430
UnsavedMarker: F8AD34FF
NavCursor: F0DFAFFF
NavWindowingHighlight: BFC9DBB0
NavWindowingDimBg: 05050533
ModalWindowDimBg: 05050559
[palette]
# Colors the app draws with that have no ImGui style slot.
accent: F0DFAFFF
accent_alt: EFEF8FFF
error: FF0000FF
error_dim: 772222FF
warning: F8AD34FF
warning_dim: 986032FF
success: 227722FF
dim: 87919DFF
neutral: 4C4C4CFF
active_window_border: F0DFAFFF

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/images/tray_icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
assets/images/tray_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
assets/libSDL3.0.dylib Normal file

Binary file not shown.

50
check_targets.jai Normal file
View File

@ -0,0 +1,50 @@
// Type-checks src/App.jai for another OS without producing or linking a binary, to catch errors
// in code your machine does not build:
//
// jai check_targets.jai - linux (from any OS)
// jai check_targets.jai - macos (from a Mac only: the Objective_C module runs code at compile time)
//
// The constants below stand in for the ones first.jai generates; keep their names in step.
#run,stallable {
set_build_options_dc(.{do_output = false});
args := get_build_options().compile_time_command_line;
target := Operating_System_Tag.LINUX;
cpu := CPU_Tag.X64;
if array_find(args, "macos") { target = .MACOS; cpu = .ARM64; }
options := get_build_options();
options.os_target = target;
options.cpu_target = cpu;
options.output_type = .NO_OUTPUT;
options.backend = .LLVM;
options.output_executable_name = "check";
options.output_path = ".build";
w := compiler_create_workspace("check");
set_build_options(options, w);
compiler_begin_intercept(w);
add_build_file("src/App.jai", w);
add_build_string(#string S
APP_NAME :: "mcap";
APP_FOLDER_NAME :: "mcap";
GIT_BRANCH :: "check";
GIT_HASH :: "-";
VERSION :: "0.0.0";
BUILD_DATE :: "-";
DEBUG :: true;
COMPILE_TESTS :: true;
COMPILE_ARGS :: "";
S, w);
while true {
message := compiler_wait_for_message();
if message.kind == .COMPLETE {
m := cast(*Message_Complete) message;
log("% check: %", target, m.error_code);
break;
}
}
compiler_end_intercept(w);
}
#import "Basic";
#import "Compiler";

326
first.jai Normal file
View File

@ -0,0 +1,326 @@
MIN_COMPILER_VERSION :: Version_Info.{ major = 0, minor = 2, micro = 30 };
MAX_COMPILER_VERSION :: Version_Info.{ major = 0, minor = 2, micro = 99 };
// Rename the app here. APP_NAME is what people see: the window title, the tray tooltip and the
// macOS bundle. APP_FOLDER_NAME names the executable and the per-user data folder, so it should be
// one lowercase word. APP_BUNDLE_IDENTIFIER is macOS's reverse-DNS id for the bundle.
APP_NAME :: "mcap";
APP_FOLDER_NAME :: "mcap";
APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
VERSION :: "0.0.0";
BUILD_DIRECTORY :: "bin";
base_options: Build_Options;
// A workspace that has been started but whose COMPLETE message has not yet been seen.
Pending_Build :: struct {
workspace: Workspace;
executable_name: string;
optimized: bool;
failed: bool;
}
#run,stallable build_project();
build_project :: () {
check_compiler_version();
#if OS == .MACOS && CPU != .ARM64 {
compiler_report("This app builds for macOS on Apple silicon (arm64) only; run a native jai, not one under Rosetta (arch -x86_64).");
}
base_options = get_build_options();
args := base_options.compile_time_command_line;
full_release := array_find(args, "full_release");
build_release := full_release || array_find(args, "release");
build_debug := full_release || !build_release;
if full_release log("Building debug and release");
else if build_release log("Building in release mode");
else log("Building in debug mode");
if build_release && array_find(args, "compile_tests") log("Compiling tests into the release build");
set_build_options_dc(.{do_output = false});
set_working_directory(#filepath); // so that we load fonts correctly.
make_directory_if_it_does_not_exist(BUILD_DIRECTORY);
// Workspaces compile concurrently once source is added, so every build is
// started before any is waited on.
pending: [..] Pending_Build;
if build_debug array_add(*pending, start_main_build(optimized = false));
if build_release array_add(*pending, start_main_build(optimized = true));
wait_for_builds(pending);
for pending if !it.failed finish_main_build(it);
// The assets are the same for every configuration, so they are copied once.
for pending if !it.failed { copy_assets(); break; }
}
wait_for_builds :: (pending: [] Pending_Build) {
remaining := pending.count;
while remaining > 0 {
message := compiler_wait_for_message();
if message.kind != .COMPLETE continue;
for * pending {
if it.workspace != message.workspace continue;
m := cast(*Message_Complete) message;
it.failed = m.error_code == .COMPILATION_FAILED;
compiler_end_intercept(it.workspace);
remaining -= 1;
}
}
}
start_main_build :: (optimized: bool) -> Pending_Build {
log("Building main executable (%)...", ifx optimized then "release" else "debug");
options := base_options;
// Builds for the compiler's own CPU; on macOS that must be arm64 (see build_project).
#if OS == .MACOS
output_executable_name_temp := tprint("%-macos", APP_FOLDER_NAME);
#if OS == .LINUX
output_executable_name_temp := tprint("%-linux", APP_FOLDER_NAME);
#if OS == .WINDOWS
output_executable_name_temp := APP_FOLDER_NAME;
if (optimized) {
set_optimization(*options, .VERY_OPTIMIZED, preserve_debug_info=true);
options.backend = .LLVM;
options.llvm_options.enable_split_modules = false;
options.output_executable_name = output_executable_name_temp;
options.array_bounds_check = .ALWAYS;
options.null_pointer_check = .ON;
options.arithmetic_overflow_check = .FATAL;
options.cast_bounds_check = .FATAL;
options.dead_code_elimination = .ALL;
} else {
#if CPU == .X64 options.backend = .X64; // the fastest to compile; LLVM elsewhere
options.output_executable_name = tprint("%-debug", output_executable_name_temp);
}
#if OS == .WINDOWS {
file_delete(tprint("%/%.pdb", BUILD_DIRECTORY, options.output_executable_name));
file_delete(tprint("%/%.rdi", BUILD_DIRECTORY, options.output_executable_name));
}
options.temporary_storage_size = 64 * 4096; // 256kB (could be more i guess?)
options.output_path = BUILD_DIRECTORY;
w := compiler_create_workspace(options.output_executable_name);
set_build_options(options, w);
#if OS == .WINDOWS {
// Disable runtime console
if optimized {
set_build_options_dc(.{append_linker_arguments=.["/SUBSYSTEM:windows", "/ENTRY:mainCRTStartup", "/IGNORE:4099"]}, w);
} else {
set_build_options_dc(.{append_linker_arguments=.["msvcprtd.lib", "/IGNORE:4099"]}, w);
}
}
compiler_begin_intercept(w);
add_build_file("src/App.jai", w);
add_build_constants(w, optimized);
return .{ workspace = w, executable_name = options.output_executable_name, optimized = optimized };
}
finish_main_build :: (build: Pending_Build) {
#if OS == .WINDOWS {
exe_path := tprint("%/%.exe", BUILD_DIRECTORY, build.executable_name);
if build.optimized {
ico_data := create_ico_file_from_bitmap_filename("assets/images/icon.png");
success := set_icon_by_data(exe_path, ico_data);
if !success {
log_error("ERROR: Couldn't set icon for '%'\n", exe_path);
}
manifest_options: Manifest_Options;
success = add_manifest_to_executable(exe_path, manifest_options);
if !success {
log_error("ERROR: Couldn't add manifest to executable '%'\n", exe_path);
}
}
} else #if OS == .MACOS {
if build.optimized create_macos_app_bundle(build.executable_name);
}
log("Built %/%", BUILD_DIRECTORY, build.executable_name);
}
#if OS == .MACOS {
MACOS_APP_BUNDLE :: #run tprint("%.app", APP_NAME);
MACOS_BUNDLE_DYLIBS :: string.["libSDL3.0.dylib"];
// The .app holds only the executable, the dylibs it loads from beside itself, the icon and
// Info.plist. The app finds data/, logs/ and images/ in the folder holding the bundle; see
// app_install_directory.
create_macos_app_bundle :: (executable_name: string) {
app := tprint("%/%", BUILD_DIRECTORY, MACOS_APP_BUNDLE);
run_command("rm", "-rf", app);
executable_folder := tprint("%/Contents/MacOS", app);
resources_folder := tprint("%/Contents/Resources", app);
make_directory_if_it_does_not_exist(executable_folder, recursive = true);
make_directory_if_it_does_not_exist(resources_folder, recursive = true);
copied := copy_file(tprint("%/%", BUILD_DIRECTORY, executable_name), tprint("%/%", executable_folder, executable_name));
for MACOS_BUNDLE_DYLIBS copied &&= copy_file(tprint("assets/%", it), tprint("%/%", executable_folder, it));
if !copied {
log_error("ERROR: Couldn't copy the executable and its dylibs into %\n", app);
return;
}
run_command("chmod", "+x", tprint("%/%", executable_folder, executable_name));
// sips makes an .icns only from a size the format holds, and icon.png may be any square size.
resized := tprint("%/icon_1024.png", BUILD_DIRECTORY);
result := run_command("sips", "-z", "1024", "1024", "assets/images/icon.png", "--out", resized);
if result.exit_code == 0 result = run_command("sips", "-s", "format", "icns", resized, "--out", tprint("%/%.icns", resources_folder, APP_FOLDER_NAME));
file_delete(resized);
if result.exit_code != 0 log_error("ERROR: Couldn't make the icon for %; the Dock will show the generic one.\n", app);
if !write_entire_file(tprint("%/Contents/Info.plist", app), tprint(MACOS_INFO_PLIST, APP_NAME, APP_NAME, executable_name, APP_BUNDLE_IDENTIFIER, APP_FOLDER_NAME, VERSION, VERSION)) {
log_error("ERROR: Couldn't write %/Contents/Info.plist\n", app);
return;
}
// Ad hoc: a signature over the whole bundle, which macOS expects, not a Developer ID.
result = run_command("codesign", "--force", "--deep", "--sign", "-", app);
if result.exit_code != 0 log_error("WARNING: Couldn't sign %; it may still run.\n", app);
log("Built %", app);
}
MACOS_INFO_PLIST :: #string PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key> <string>en</string>
<key>CFBundleDisplayName</key> <string>%</string>
<key>CFBundleName</key> <string>%</string>
<key>CFBundleExecutable</key> <string>%</string>
<key>CFBundleIdentifier</key> <string>%</string>
<key>CFBundleIconFile</key> <string>%.icns</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundlePackageType</key> <string>APPL</string>
<key>CFBundleShortVersionString</key> <string>%</string>
<key>CFBundleVersion</key> <string>%</string>
<key>NSHighResolutionCapable</key> <true/>
</dict>
</plist>
PLIST
}
copy_assets :: () {
copy_directory("assets", "bin"); // This puts the CONTENTS of assets into bin. Not sure if that's what we want, but I'll roll with it.
#if OS == .MACOS copy_file("assets/libSDL3.0.dylib", "bin/libSDL3.0.dylib");
}
add_build_constants :: (w: Workspace, optimized: bool) {
BUILD_DATE :: #run -> string {
current_time := to_calendar(current_time_consensus());
return calendar_to_string(current_time);
}
GIT_HASH :: #run -> string {
result, revision := run_command("git", "describe", "--always", "--dirty", capture_and_return_output = true);
if revision return trim(revision);
return "-";
}
GIT_BRANCH :: #run -> string {
result, branch_name := run_command("git", "branch", "--show-current", capture_and_return_output = true);
return trim(branch_name);
}
build_constants := tprint(#string STRING
APP_NAME :: "%";
APP_FOLDER_NAME :: "%";
GIT_BRANCH :: "%";
GIT_HASH :: "%";
VERSION :: "%";
BUILD_DATE :: "%";
DEBUG :: %;
COMPILE_TESTS :: %;
COMPILE_ARGS :: "%";
STRING,
escape_for_string_literal(APP_NAME),
escape_for_string_literal(APP_FOLDER_NAME),
escape_for_string_literal(GIT_BRANCH),
escape_for_string_literal(GIT_HASH),
VERSION,
BUILD_DATE,
ifx optimized then "false" else "true",
ifx !optimized || array_find(base_options.compile_time_command_line, "compile_tests") then "true" else "false",
escape_for_string_literal(join(..base_options.compile_time_command_line, " "))
);
add_build_string(build_constants, w);
}
check_compiler_version :: () {
version_info: Version_Info;
version_str := compiler_get_version_info(*version_info);
min_version_str := tprint("%.%.%", MIN_COMPILER_VERSION.major, MIN_COMPILER_VERSION.minor, MIN_COMPILER_VERSION.micro);
max_version_str := tprint("%.%.%", MAX_COMPILER_VERSION.major, MAX_COMPILER_VERSION.minor, MAX_COMPILER_VERSION.micro);
if compare_versions(version_info, MIN_COMPILER_VERSION) < 0 {
error := tprint("Compiler version '%' is below the minimum required version '%'", version_str, min_version_str);
compiler_report(error);
}
if compare_versions(version_info, MAX_COMPILER_VERSION) > 0 {
error := tprint("Compiler version '%' is above the maximum supported version '%'", version_str, max_version_str);
compiler_report(error);
}
}
// Lexicographic: a later component only matters when every earlier one is equal.
compare_versions :: (a: Version_Info, b: Version_Info) -> int {
if a.major != b.major return ifx a.major < b.major then -1 else 1;
if a.minor != b.minor return ifx a.minor < b.minor then -1 else 1;
if a.micro != b.micro return ifx a.micro < b.micro then -1 else 1;
return 0;
}
// For text spliced between double quotes in generated source.
escape_for_string_literal :: (s: string) -> string {
builder: String_Builder;
builder.allocator = temp;
for 0..s.count-1 {
c := s[it];
if c == {
case #char "\\"; append(*builder, "\\\\");
case #char "\""; append(*builder, "\\\"");
case #char "\n"; append(*builder, "\\n");
case #char "\r"; append(*builder, "\\r");
case; append(*builder, c);
}
}
return builder_to_string(*builder,, temp);
}
#import "Basic";
#import "Compiler";
#import "String";
#import "File";
#import "Process";
#import "File_Utilities";
#if OS == .WINDOWS {
#import "Ico_File";
#import "Windows_Resources";
#import "Windows_Utf8";
}

File diff suppressed because it is too large Load Diff

346
modules/Basic/Array.jai Normal file
View File

@ -0,0 +1,346 @@
/*
Because arrays are built-in types in this language, for people
to have high morale when using them, things need to be very clear.
The set of basic operations included here, that most programs are
expected to use, should be simple and small. People should be able
to read this file and have no questions. They should know exactly
what is going to happen when they use one of these arrays in their
program.
Simplicity is always good, but it is ***CRITICAL*** in this file,
because without it, people will just stumble through life not really
knowing what their code does, or even what it is trying to do --
just like most modern programming languages. We want to avoid that.
This is not meant to contain anything people might just want to do
with arrays. It's supposed to be a minimal and clear set of
functions that let you do the most universal things.
Please don't add new things to this file without checking with me.
-jblow, 20 July 2019
*/
/*
These routines operate on [] and [..] arrays.
These two kinds of arrays are straightforwardly defined, and are
equivalent to small structs. You can look in modules/Preload.jai
for the declarations: [] is equivalent to Array_View_64,
and [..] is equivalent to Resizable_Array.
*/
// Now that we have namespacing, we can consider removing the array_ prefixes here.
// Maybe we want better renamers before we consider that too seriously, though.
array_copy :: (array: [] $T) -> [] T {
dest := NewArray(array.count, T, initialized=false);
memcpy(dest.data, array.data, array.count * size_of(T));
return dest;
}
// This seems unnecessary. A couple people use it, but ... bleah.
array_copy :: (dest: *[..] $T, source: [] T) {
array_reserve(cast(*Resizable_Array) dest, source.count, size_of(T));
memcpy(dest.data, source.data, source.count * size_of(T));
dest.count = source.count;
}
array_view :: inline (array: [] $T, index: int) -> [] T {
return array_view(array, index, array.count - index);
}
array_view :: inline (array: [] $T, index: int, count: int) -> [] T {
assert(index >= 0, "index = %", index); // @@ Should we also clamp in these cases?
assert(count >= 0, "count = %", count);
if index >= array.count {
return .[];
}
if index + count > array.count {
count = array.count - index;
}
result: [] T = ---;
result.data = array.data + index;
result.count = count;
return result;
}
array_free :: inline (array: [] $T) {
free(array.data);
}
// @@ IC: Note that table_reset doesn't free memory, and it's confusing that array_reset does. For dynamic arrays we have array_reset and array_reset_keeping_memory, which is verbose, and as long as you
// are aware of the second function, the behavior is clear, but maybe it should be the other way around. array_reset doesn't free memory, while array_reset_and_free does. Or maybe we could follow
// STL's convention and use the term 'clear' for removing elements without freeing memory. So we would have:
// Static array: array_reset
// Dynamic array: array_clear, array_reset
// Table: table_clear, table_reset
array_reset :: (array: *[] $T) {
free(array.data);
array.count = 0;
array.data = null;
}
//
// Adding things to an array:
//
array_add :: (array: *[..] $T, item: T) #no_abc {
maybe_grow(xx array, size_of(T));
(array.data + array.count).* = item; // Note, we don't have an ABC here since we believe maybe_grow does the right thing.
array.count += 1;
}
array_add :: (array: *[..] $T, to_append: ..T) { // Multi-argument version. Single-argument will be chosen in overload resolution for appropriate cases!
// Inlined version of maybe_grow that expands for to_append.count > 1...
new_count := array.count + to_append.count;
if new_count > array.allocated {
reserve := max(8, 2 * new_count);
array_reserve(cast(*Resizable_Array) array, reserve, size_of(T));
}
memcpy(array.data + array.count, to_append.data, to_append.count * size_of(T));
array.count += to_append.count;
}
array_add :: (array: *[..] $T) -> *T {
maybe_grow(xx array, size_of(T));
result := array.data + array.count;
ini :: initializer_of(T);
#if ini {
ini(result);
} else {
memset(result, 0, size_of(T));
}
array.count += 1;
return result;
}
array_add_if_unique :: (array: *[..] $T, item: T, $compare: (T, T) -> bool = null) -> bool, s64 { // Return true if newly added; also return index.
// @Compiler: We should not need explicit dereference here, but auto-dereference doesn't work in some cases.
found, index := array_find(array.*, item, compare);
if found return false, index;
array_add(array, item);
return true, array.count-1;
}
array_insert_at :: (array: *[..] $T, item: T, index: int) {
assert(index >= 0);
assert(index <= array.count);
maybe_grow(xx array, size_of(T));
array.count += 1;
i := array.count-1;
while i > index {
(array.data + i).* = (array.data + i - 1).*;
i -= 1;
}
(array.data + index).* = item;
}
//
// Removing things:
//
array_unordered_remove_by_pointer :: inline (array: *[]$T, item_pointer: *T) {
assert(item_pointer >= array.data && item_pointer < (array.data + array.count));
index := item_pointer - array.data;
array_unordered_remove_by_index(array, index);
}
array_unordered_remove_by_value :: inline (array: *[] $T, item: T, $stop_after_first := false, $compare: (T, T) -> bool = null) -> s64 {
// Probably want to say something to debug memory trackers here.
// Need to make sure it doesn't cost anything in Release.
removed := 0;
for array.* {
matching := #ifx compare {
compare(it, item);
} else {
it == item;
}
if matching {
removed += 1;
remove it;
#if stop_after_first break; // Early-out optimization.
}
}
return removed;
}
array_unordered_remove_by_index :: inline (array_pointer: *[] $T, index: s64) {
assert(index >= 0);
assert(index < array_pointer.count);
array := array_pointer.*;
last_index := array_pointer.count - 1;
if index != last_index array[index] = array[last_index];
array_pointer.count -= 1;
}
// array_ordered_remove_by_value removes the first occurrence only.
// We could consider having array_ordered_remove_all_by_value as well.
array_ordered_remove_by_value :: inline (array_pointer: *[] $T, item: T) -> bool {
found, index := array_find(array_pointer.*, item);
if found array_ordered_remove_by_index(array_pointer, index);
return found;
}
array_ordered_remove_by_index :: inline (array_pointer: *[] $T, index: s64) {
assert(index >= 0);
assert(index < array_pointer.count);
array := array_pointer.*;
for i : index..array.count-2
array[i] = array[i+1];
array_pointer.count -= 1;
}
array_find :: (array: [] $T, item: T, $compare: (T, T) -> bool = null) -> bool, s64 {
#if compare {
for array if compare(it, item) return true, it_index;
} else {
for array if it == item return true, it_index;
}
return false, -1; // Not found.
}
//
// Treating an array like a stack:
//
peek :: inline (array: [] $T) -> T {
return array[array.count-1];
}
peek_pointer :: inline (array: [] $T) -> *T {
return *array[array.count-1];
}
pop :: (array: *[] $T) -> T {
// If we weren't worried about tripping an array bounds check, we wouldn't have to use
// this temporary copy of 'result', but possibly a future change in the way we
// return values may make it not matter.
result := array.*[array.count-1];
array.count -= 1;
return result;
}
//
// Managing array memory:
//
array_free :: (array: [..] $T) {
free(array.data,, array.allocator);
}
array_reset :: (array: *[..] $T) {
free(array.data,, array.allocator);
array.data = null;
array.allocated = 0;
array.count = 0;
}
array_reset_keeping_memory :: inline (array: *[..] $T) {
array.count = 0;
}
array_reserve :: inline (array: *[..] $T, desired_items: s64) {
array_reserve(cast(*Resizable_Array) array, desired_items, size_of(T));
}
// This overload of array_reserve takes a non-polymorphic *Resizable_Array,
// which will match any array type (since only the size of the data _pointed at_
// by the array changes with T). We downcast to *Resizable_Array because
// when a leaf like this has many more polymorphs than necessary (due to the size_of(T)
// in vanilla array_reserve() being a constant), it causes everyone up the chain to
// have more polymorphs than necessary, because they need to call different leaves!
array_reserve :: inline (array: *Resizable_Array, desired_items: s64, element_size: s64) {
if desired_items <= array.allocated return;
if !array.allocator.proc remember_allocators(array);
array.data = realloc(array.data, desired_items * element_size, array.allocated * element_size,, array.allocator);
assert(array.data != null); // The program will die if we couldn't get memory. We can think about an alternative strategy, but it's hard to know what would be reasonable.
array.allocated = desired_items;
}
//
// The following two routines are not part of the basic [..] operation above.
// They are here for end-user convenience.
//
array_resize :: (array: *[] $T, new_count: s64, $initialize:=true) {
old_count := array.count;
if old_count == new_count return;
array.data = cast(*T) realloc(array.data, new_count * size_of(T), array.count * size_of(T));
array.count = new_count;
#if initialize init_range(array.data, old_count, new_count);
}
array_resize :: (array: *[..] $T, new_count: s64, $initialize:=true) {
old_count := array.count;
if old_count == new_count return;
array_reserve(cast(*Resizable_Array) array, new_count, size_of(T));
array.count = new_count;
#if initialize init_range(array.data, old_count, new_count);
}
// maybe_grow is intended just to check when array.count == array.allocated;
// if count > allocated, the array is in an invalid state. You should not do this,
// but just for extreme paranoia reasons, we use >= just to try to have better
// behavior in buggy programs.
//
// When calling maybe_grow, we cast down to *Resizable_Array, which means
// we can call this function without knowing T at compile-time. This makes
// the calling sites in this file slightly uglier, but maybe that's okay.
maybe_grow :: inline (array: *Resizable_Array, element_size: s64) {
if array.count >= array.allocated {
reserve := max(8, 2 * array.count);
array_reserve(cast(*Resizable_Array) array, reserve, element_size);
}
}
#scope_file
init_range :: (ptr: *$T, begin: s64, end: s64) {
ini :: initializer_of(T);
#if ini {
for begin..end-1 inline ini(ptr + it); // Really this can be one ini followed by a bunch of memcpy. For long arrays we could power-of-two double the copy out, etc.
} else {
count := end - begin;
if count <= 0 return;
// Fast path for init to zero.
// In the case that we are shrinking an array, count will be negative.
memset(ptr + begin, 0, count * size_of(T));
}
}

840
modules/Basic/Int128.jai Normal file
View File

@ -0,0 +1,840 @@
// 128-bit signed integer:
S128 :: struct {
low: u64;
high: s64;
}
// 128-bit unsigned integer:
U128 :: struct {
low: u64;
high: u64;
}
// These values returned for quotient and remainder of division when overflow or division by zero
U128_ERROR :: U128.{0xffffffff_ffffffff, 0xffffffff_ffffffff};
S128_ERROR :: S128.{0xffffffff_ffffffff, 0xffffffff_ffffffff};
// If you want a 128-bit result, well, we don't have any primitive types
// that hold that, so, here is some code to do it within u64.
// We might put an asm version here, but right now this is used to
// avoid asm stuff...!
multiply_64bits_with_overflow :: (a: u64, b: u64) -> (high: u64, low: u64) {
lo_lo := (a & 0xFFFF_FFFF) * (b & 0xFFFF_FFFF);
hi_lo := (a >> 32) * (b & 0xFFFF_FFFF);
lo_hi := (a & 0xFFFF_FFFF) * (b >> 32);
hi_hi := (a >> 32) * (b >> 32);
cross := (lo_lo >> 32) + (hi_lo & 0xFFFF_FFFF) + lo_hi;
high := (hi_lo >> 32) + (cross >> 32) + hi_hi;
low := (cross << 32) | (lo_lo & 0xFFFF_FFFF);
return high, low;
}
to_s128 :: (a: $T) -> S128
#modify {
return (cast(*Type_Info)T).type == .INTEGER;
} {
return .{cast,no_check(u64) a, (cast(s64)a) >> 63};
}
to_u128 :: (a: $T) -> U128
#modify {
return T == u64 || T == u32 || T == u16 || T == u8;
} {
return U128.{a, 0};
}
operator == :: inline (a: S128, b: S128) -> bool {
return (a.low == b.low) && (a.high == b.high);
}
operator == :: inline (a: U128, b: U128) -> bool {
return (a.low == b.low) && (a.high == b.high);
}
operator ! :: inline (a: S128) -> bool {
return a.low == 0 && a.high == 0;
}
operator ! :: inline (a: U128) -> bool {
return a.low == 0 && a.high == 0;
}
operator + :: inline (a: S128, b: S128) -> S128 { // This can be the same code for S128 and U128.
c: S128 = ---;
#if CPU == .X64 {
#asm {
mov a_low:, [*a + 0];
mov a_high:, [*a + 8];
mov b_low:, [*b + 0];
mov b_high:, [*b + 8];
mov [*c + 0], a_low;
mov [*c + 8], a_high;
add [*c + 0], b_low;
adc [*c + 8], b_high;
}
} else {
#no_aoc { // Let's just no_aoc this one operation for now. In theory high bits should not overflow.
c.low = a.low + b.low;
}
c.high = a.high + b.high;
if c.low < a.low c.high += 1; // We overflowed!
}
return c;
}
operator - :: inline (a: S128, b: S128) -> S128 {
return cast,force(S128) (cast,force(U128)a - cast,force(U128)b);
}
operator - :: inline (a: U128, b: U128) -> U128 {
c: U128 = ---;
#if CPU == .X64 {
#asm {
mov a_low:, [*a + 0];
mov a_high:, [*a + 8];
mov b_low:, [*b + 0];
mov b_high:, [*b + 8];
mov [*c + 0], a_low;
mov [*c + 8], a_high;
sub [*c + 0], b_low;
sbb [*c + 8], b_high;
// I guess these are not legal forms of 'add':
// add [*c + 0], a_low, b_low;
// adc [*c + 8], a_high, b_high;
}
} else #no_aoc {
c.high = a.high - b.high;
c.low = a.low - b.low;
if c.low > a.low c.high -= 1; // We underflowed!
}
return c;
}
operator - :: inline (a: S128) -> S128 {
// Apollo Time is two's complement.
// To negate it, we flip all the bits and then add 1.
// We'll use #asm to avoid a branch.
c: S128 = ---;
c.low = ~a.low;
c.high = ~a.high;
#if CPU == .X64 {
#asm {
add [*c + 0], 1;
adc [*c + 8], 0;
}
} else {
#no_aoc {
c.low += 1;
}
if !c.low c.high += 1; // If c.low overflowed, carry into high.
}
return c;
}
// Complexity-wise, it may not be worth specializing << and >> for constant x,
// since the win is relatively low these days. But I did it anyway.
operator << :: (a: S128, $$x: u8) -> S128 {
result : S128 = ---;
if x >= 64 {
new_high := cast,no_check(s64)(a.low << (x - 64));
return .{0, new_high};
} else {
#if CPU == .X64 {
result = a;
#if !is_constant(x) #asm { x === c; } // Use the cl form of shift.
#asm {
mov low:, [*result + 0];
shld [*result + 8], low, x;
shl [*result + 0], x;
}
} else {
result.high = a.high << x | cast(s64)(a.low >> (64 - x));
result.low = a.low << x;
}
}
return result;
}
operator << :: inline (a: U128, $$x: u8) -> U128 {
result : U128 = ---;
if x >= 64 {
new_high := a.low << (x - 64);
return .{0, new_high};
} else {
#if CPU == .X64 {
result = a;
#if !is_constant(x) #asm { x === c; } // Use the cl form of shift.
#asm {
mov low:, [*result + 0];
shld [*result + 8], low, x;
shl [*result + 0], x;
}
} else {
result.high = a.high << x | (a.low >> (64 - x));
result.low = a.low << x;
}
}
return result;
}
operator >> :: (a: S128, $$x: u8) -> S128 { // Arithmetic shift
if x >= 64 {
new_low := cast,no_check (u64) (a.high >> (x - 64));
return .{low = new_low, high = a.high >> 63};
} else {
#if CPU == .X64 {
result := a;
#if !is_constant(x) #asm { x === c; } // Use the cl form of shift.
#asm {
mov high:, [*result + 8];
shrd [*result + 0], high, x;
sar [*result + 8], x;
}
} else {
result: S128 = ---;
result.low = (a.low >> x) | cast(u64)(a.high << (64 - x));
result.high = a.high >> x;
}
return result;
}
}
operator >> :: (a: U128, $$x: u8) -> U128 { // Logical shift.
if !a || !x return a;
if x >= 64 {
return .{low = a.high >> (x - 64), high = 0};
} else {
#if CPU == .X64 {
result := a;
#if !is_constant(x) #asm { x === c; } // Use the cl form of shift.
#asm {
mov high:, [*result + 8];
shrd [*result + 0], high, x;
shr [*result + 8], x;
}
} else {
result: U128 = ---;
result.low = a.low >> x | (a.high << (64 - x));
result.high = a.high >> x;
}
return result;
}
}
// @Incomplete: Refactor logical_shift_right as >> on U128.
logical_shift_right :: (a: S128, $$x: u8) -> S128 {
return cast,force(S128) ((cast,force(U128) a) >> x);
}
// Our division procs handle divide-by-zero and overflow without aborting, returning an error flag and
// setting the result to U128_MAX. While using operator `/` you don't get this error flag, and the expected
// result of any operator `/` divide-by-zero is boom, so here we'll assert to simulate that behaviour.
// OTOH, overflow is bit weird and not as easy to anticipate, so we'll allow it to return (and the programmer can
// test for the error return value when they notice weird behaviour with their results, but their program won't
// unexpectedly crash in production when the user puts in some bad values).
// Maybe it'd be better to halt on both conditions, with the programmer expected to validate the inputs before
// calling, but the easiest way for them to do that would be for them to call the (un)signed_divide_with_remainder
// proc themselves... at which point they don't need the `/` operator, so we're letting the `/` operator be the
// "easy use" version of divide. - IK
operator / :: (dividend: U128, divisor: U128) -> U128 {
assert(divisor != .{0, 0}, "Cannot divide by zero!");
return inline unsigned_divide_with_remainder(dividend, divisor);
}
operator / :: (dividend: S128, divisor: S128) -> S128 {
assert(divisor != .{0, 0}, "Cannot divide by zero!");
return inline signed_divide_with_remainder(dividend, divisor);
}
operator / :: inline (a: S128, factor: s64) -> S128 {
// We'll put a faster version here later?
b: S128;
negate := false;
b.low = cast,no_check(u64) factor;
if factor < 0 b.high = -1;
return a / b;
}
signed_divide_with_remainder :: inline (dividend: S128, divisor: S128) -> quotient: S128, remainder: S128, overflow_or_divide_by_zero: bool {
u := dividend;
v := divisor;
negate := false;
if u.high < 0 {
negate = true;
u = -u;
}
if v.high < 0 {
negate = !negate;
v = -v;
}
quotient, remainder, overflow_or_divide_by_zero := unsigned_divide_with_remainder(cast,force(U128)u, cast,force(U128)v);
if overflow_or_divide_by_zero return cast,force(S128)quotient, cast,force(S128)remainder, true;
else if negate return -cast,force(S128)quotient, cast,force(S128)remainder, false;
else return cast,force(S128)quotient, cast,force(S128)remainder, false;
}
unsigned_divide_with_remainder :: (u: U128, v: U128) -> quotient: U128, remainder: U128, overflow_or_divide_by_zero: bool {
// This unsigned divide is used to help implement our operator /, which is
// a wrapper that does sign manipulations then calls this routine.
// Code adapted from Hacker's Delight by Henry S. Warren, Jr., Second Edition, section 9-5.
// That listing was for 64-bit division, but here we do 128-bit division.
if !v return U128_ERROR, U128_ERROR, true;
#if CPU == .X64 { // Lifted these procs out of here so they can be tested against each other
DIVU_FF :: unsigned_divide_with_remainder_128_64_x64;
} else {
DIVU_FF :: unsigned_divide_with_remainder_128_64_no_asm;
}
if v.high == 0 {
u1 := cast,no_check(u64) u.high;
d := v.low;
if u1 < d { // If u/v cannot overflow, just do one division.
quotient: U128 = ---;
remainder: U128 = ---;
quotient.high, remainder.high = 0;
error : bool = ---;
quotient.low, remainder.low, error = DIVU_FF(u, d);
if error return U128_ERROR, U128_ERROR, true;
else return quotient, remainder, false;
} else {
// If u/v would overflow: Break up u into two halves.
u0 := u.low;
// q1 := DIVU_FF(u1, d); // First quotient digit.
q1 := u1 / d; // First quotient digit.
k := u1 - q1*d; // First remainder, < v.
c: U128 = ---;
c.high = k;
c.low = u0;
q0, remainder_u64, error := DIVU_FF(c, d);
quotient := U128.{low = q0, high = q1};
remainder := U128.{low = remainder_u64, high = 0};
if error return U128_ERROR, U128_ERROR, true;
else return quotient, remainder, false;
}
}
n := count_leading_zero_bits(v.high);
_v1 := v << n;
// Warren shifts right here to get the top bits, but we can just use .high
v1 := cast(u64) _v1.high;
// Right-shift u by 1 bit, to ensure no overflow.
u1 := (u >> 1);
q1, _, error := DIVU_FF(u1, v1);
if error return U128_ERROR, U128_ERROR, true;
// Undo normalization and division of u by 2.
_q1 := U128.{low = q1};
_q1 <<= n;
q0 := _q1 >> 63;
// Make q0 correct or too small by 1.
if q0.high || q0.low { // q0 != 0
if q0.low == 0 {
// Borrow from the top; could use #asm.
q0.low -= 1;
q0.high -= 1;
} else {
q0.low -= 1;
}
}
remainder := u - q0*v;
if remainder >= v { // Finally correct q0.
q0.low += 1;
if q0.low == 0 q0.high += 1;
remainder -= v;
}
return q0, remainder, false;
}
#scope_file
U64_MAX :: 0xFFFF_FFFF_FFFF_FFFF;
#if CPU == .X64 {
unsigned_divide_with_remainder_128_64_x64 :: (a: U128, d: u64) -> quotient: u64, remainder: u64, overflow_or_divide_by_zero: bool {
#assert CPU == .X64;
if a.high >= d
return U64_MAX, U64_MAX, true;
quotient: u64 = ---;
remainder: u64 = ---;
#asm {
high: gpr === d;
low: gpr === a;
mov low, [*a + 0];
mov high, [*a + 8];
div high, low, d;
mov quotient, low;
mov remainder, high;
}
return quotient, remainder, false;
}
}
unsigned_divide_with_remainder_128_64_no_asm :: (num: U128, den: u64) -> quotient: u64, remainder: u64, overflow_or_divide_by_zero: bool #no_aoc {
// This software version is derived from public domain/CC0 code found at:
// https://ridiculousfish.com/blog/posts/labor-of-division-episode-v.html
numhi := num.high;
numlo := num.low;
// Check for overflow and divide by 0.
if numhi >= den
return U64_MAX, U64_MAX, true;
// We work in base 2**32.
// A uint32 holds a single digit. A uint64 holds two digits.
// Our numerator is conceptually [num3, num2, num1, num0].
// Our denominator is [den1, den0].
b : u64 : (1 << 32);
// The high and low digits of our computed quotient.
q1: u32 = ---;
q0: u32 = ---;
// The normalization shift factor.
shift: int = ---;
// The high and low digits of our denominator (after normalizing).
// Also the low 2 digits of our numerator (after normalizing).
den1: u32 = ---;
den0: u32 = ---;
num1: u32 = ---;
num0: u32 = ---;
// A partial remainder.
rem: u64 = ---;
// The estimated quotient, and its corresponding remainder (unrelated to true remainder).
qhat: u64 = ---;
rhat: u64 = ---;
// Variables used to correct the estimated quotient.
c1: u64 = ---;
c2: u64 = ---;
// Determine the normalization factor. We multiply den by this, so that its leading digit is at
// least half b. In binary this means just shifting left by the number of leading zeros, so that
// there's a 1 in the MSB.
// We also shift numer by the same amount. This cannot overflow because numhi < den.
// The expression (-shift & 63) is the same as (64 - shift), except it avoids the UB of shifting
// by 64. The funny bitwise 'and' ensures that numlo does not get shifted into numhi if shift is 0.
// clang 11 has an x86 codegen bug here: see LLVM bug 50118. The sequence below avoids it.
shift = count_leading_zero_bits(den);
den <<= cast(u64)shift;
numhi <<= cast(u64)shift;
numhi |= (numlo >> (-shift & 63)) & cast,no_check(u64)(-shift >> 63);
numlo <<= cast(u64)shift;
// Extract the low digits of the numerator and both digits of the denominator.
num1 = cast(u32)(numlo >> 32);
num0 = cast(u32)(numlo & 0xFFFF_FFFF);
den1 = cast(u32)(den >> 32);
den0 = cast(u32)(den & 0xFFFF_FFFF);
// We wish to compute q1 = [n3 n2 n1] / [d1 d0].
// Estimate q1 as [n3 n2] / [d1], and then correct it.
// Note while qhat may be 2 digits, q1 is always 1 digit.
qhat = numhi / den1;
rhat = numhi % den1;
c1 = qhat * den0;
c2 = rhat * b + num1;
if c1 > c2
qhat -= ifx c1 - c2 > den then cast(u64)2 else 1;
q1 = cast(u32)qhat;
// Compute the true (partial) remainder.
rem = numhi * b + num1 - q1 * den;
// We wish to compute q0 = [rem1 rem0 n0] / [d1 d0].
// Estimate q0 as [rem1 rem0] / [d1] and correct it.
qhat = rem / den1;
rhat = rem % den1;
c1 = qhat * den0;
c2 = rhat * b + num0;
if c1 > c2
qhat -= ifx c1 - c2 > den then cast(u64)2 else 1;
q0 = cast(u32)qhat;
quotient := ((cast(u64)q1) << 32) | q0;
remainder := (rem * b + num0 - q0 * den) >> shift;
return quotient, remainder, false;
}
count_leading_zero_bits :: inline (x: u64) -> u8 {
// Maybe Basic should have a Simple_Math component with stuff like this in it? - IK
n: u8 = ---;
#if CPU == .X64 {
#asm {
lzcnt n, x;
}
} else {
if x == 0 return(64);
n = 0;
if x <= 0x00000000FFFFFFFF { n = n + 32; x = x << 32; }
if x <= 0x0000FFFFFFFFFFFF { n = n + 16; x = x << 16; }
if x <= 0x00FFFFFFFFFFFFFF { n = n + 8; x = x << 8; }
if x <= 0x0FFFFFFFFFFFFFFF { n = n + 4; x = x << 4; }
if x <= 0x3FFFFFFFFFFFFFFF { n = n + 2; x = x << 2; }
if x <= 0x7FFFFFFFFFFFFFFF { n = n + 1; }
}
return n;
}
#scope_export
//
// The sign handling in these operator * below is a little uncertain given
// that I don't really understand the difference between imul and mul.
// @Incomplete: Consult Hacker's Delight.
//
operator * :: (_a: S128, _b: S128) -> S128 {
a := _a; // @Speed @Temporary
b := _b;
negate := false;
if a.high < 0 {
a = -a;
negate = true;
}
if b.high < 0 {
b = -b;
negate = !negate;
}
u := cast,force(U128)a * cast,force(U128) b;
result := cast,force(S128) u;
if negate return -result;
else return result;
}
operator * :: (_a: U128, _b: U128) -> U128 {
a := _a; // @Speed @Temporary
b := _b;
result: U128 = ---;
// The code below is really convoluted because I am not sure if we can
// bind multiple names to the same physical register? Would really like to.
#if CPU == .X64 {
#asm {
result_high: gpr === d;
result_low: gpr === a;
factor: gpr;
high_sum: gpr;
// Low times low:
mov result_low, [*a + 0]; // This is input as well as result.
mov factor, [*b + 0];
mul result_high, result_low, factor;
mov [*result + 0], result_low; // Store the low part.
mov high_sum, result_high; // Store the high part.
// High a times low b. 'factor' still holds low b.
mov result_low, [*a + 8];
imul result_high, result_low, factor;
add high_sum, result_low;
// Low a times high b.
mov result_low, [*a + 0];
mov factor, [*b + 8];
imul result_high, result_low, factor;
add high_sum, result_low;
// High a times high b is discarded.
mov [*result + 8], high_sum;
}
} else {
// Intentionally leaving aoc enabled here, since these should not in principle overflow.
result.high, result.low = multiply_64bits_with_overflow(a.low, b.low);
result.high += a.high * b.low + a.low * b.high;
}
return result;
}
operator * :: (a: S128, factor: s64) -> S128 #symmetric {
at := a; // @Speed @Temporary
negate := false;
if at.high < 0 {
at = -at;
negate = true;
}
if factor < 0 {
factor = -factor;
negate = !negate;
}
result: S128 = ---;
// We would like to be able to bind multiple names to the same machine register
// to make code like the following less confusing.
#if CPU == .X64 {
#asm {
result_high: gpr === d;
result_low: gpr === a;
mov result_low, [*at + 0]; // This is input as well as result.
mul result_high, result_low, factor;
mov [*result + 0], result_low;
mov carry:, result_high;
mov result_low, [*at + 8]; // This is input as well as result.
imul result_high, result_low, factor;
add result_low, carry;
mov [*result + 8], result_low; // This is actually the high.
}
} else #no_aoc {
u128 : U128 = ---;
u128.high, u128.low = multiply_64bits_with_overflow(at.low, cast(u64)factor);
u128.high += cast(u64)at.high * cast(u64)factor;
result = cast,force(S128)u128;
}
if negate return -result;
else return result;
}
operator < :: (a: S128, b: S128) -> bool {
result: bool = ---;
#if CPU == .X64 {
#asm {
mov a_low:, [*a + 0];
mov a_high:, [*a + 8];
mov b_low:, [*b + 0];
mov b_high:, [*b + 8];
sub a_low, b_low;
sbb a_high, b_high;
setl result;
}
} else {
if a.high < b.high return true;
if a.high > b.high return false;
return a.low < b.low;
}
return result;
}
operator < :: (a: U128, b: U128) -> bool { // @Speed: Do #asm.
if (a.high < b.high) return true;
if (a.high > b.high) return false;
return (a.low < b.low);
}
operator <= :: (a: S128, b: S128) -> bool {
// We would like the compiler to generate this for us, probably, like it does with +=.
return (a == b) || (a < b); // Probably not very efficient.
}
operator > :: (a: S128, b: S128) -> bool {
// We would like the compiler to generate this for us, probably, like it does with +=.
return !(a <= b);
}
operator >= :: (a: S128, b: S128) -> bool {
// We would like the compiler to generate this for us, probably, like it does with +=.
return !(a < b);
}
operator <= :: (a: U128, b: U128) -> bool {
// We would like the compiler to generate this for us, probably, like it does with +=.
return (a == b) || (a < b); // Probably not very efficient.
}
operator > :: (a: U128, b: U128) -> bool {
// We would like the compiler to generate this for us, probably, like it does with +=.
return !(a <= b);
}
operator >= :: (a: U128, b: U128) -> bool {
// We would like the compiler to generate this for us, probably, like it does with +=.
return !(a < b);
}
operator * :: (a: U128, b: s64) -> U128 {
return a * cast(u64)b;
}
operator * :: (a: U128, b: u64) -> U128 {
b128 : U128 = ---;
b128.high = 0;
b128.low = b;
return a * b128;
}
operator + :: (a: U128, b: U128) -> U128 {
return cast,force(U128)(cast,force(S128)a + cast,force(S128)b);
}
operator + :: (a: U128, b: u64) -> U128 {
return a + U128.{b, 0};
}
#if CPU == .X64 {
test_unsigned_division :: (random_iterations := 0, random_get : () -> u64 = null) {
to_string :: (x: U128) -> string {
if x.high
return tprint("0x%0_%0", formatInt(x.high, base=16), formatInt(x.low, base=16, minimum_digits=16));
else
return tprint("%0", x.low);
}
test_pattern : u64 : 0xAAAAAAAA_AAAAAAAA;
dividend := U128.{test_pattern, test_pattern};
// Test x64 version against no_asm version - both should give identical results!
// Also test the result played backwards through multiply gives the start point.
while true {
divisor := test_pattern;
while true {
x64_quotient, x64_remainder, x64_error := unsigned_divide_with_remainder_128_64_x64(dividend, divisor);
jai_quotient, jai_remainder, jai_error := unsigned_divide_with_remainder_128_64_no_asm(dividend, divisor);
assert(x64_quotient == jai_quotient);
assert(x64_remainder == jai_remainder);
assert(x64_error == jai_error);
if !x64_error {
print("[OK] % / % = % remainder %\n", to_string(dividend), divisor, x64_quotient, x64_remainder);
assert(U128.{divisor, 0} * x64_quotient + x64_remainder == dividend);
}
if !divisor break;
divisor >>= 1;
}
if !dividend break;
dividend >>= 1;
}
// Test `/` operator
dividend = U128.{test_pattern, test_pattern};
while true {
divisor := U128.{test_pattern, test_pattern};
while true {
quotient, remainder, overflow_or_div_zero := unsigned_divide_with_remainder(dividend, divisor);
operator_quotient := dividend / divisor;
if overflow_or_div_zero {
print("[OVR] % / %\n", to_string(dividend), to_string(divisor));
assert(operator_quotient.high == U64_MAX && operator_quotient.low == U64_MAX);
} else {
print("[OK] % / % = % remainder %\n", to_string(dividend), to_string(divisor), to_string(quotient), to_string(remainder));
assert(quotient * divisor + remainder == dividend);
assert(quotient == operator_quotient);
operator_remainder := dividend - (operator_quotient * divisor);
assert(remainder == operator_remainder);
if quotient == .{}
assert(remainder == dividend);
}
divisor >>= 1; // Currently operator `/` halts on divide-by-zero - if that behaviour changes then swap this line and the one below it around
if !divisor break;
}
if !dividend break;
dividend >>= 1;
}
// Test randomly
for 1 .. random_iterations {
assert(random_get != null);
x := U128.{low = random_get(), high = 0};
y := U128.{low = random_get(), high = 0};
r := U128.{low = random_get() % x.low, high = 0};
dividend = x * y + r;
divisor := x;
quotient, remainder, overflow_or_div_zero := unsigned_divide_with_remainder(dividend, divisor);
operator_quotient := dividend / divisor;
if overflow_or_div_zero {
print("[OVR] % / %\n", to_string(dividend), to_string(divisor));
assert(operator_quotient.high == U64_MAX && operator_quotient.low == U64_MAX);
} else {
print("[OK] % / % = % remainder %\n", to_string(dividend), to_string(divisor), to_string(quotient), to_string(remainder));
assert(quotient * divisor + remainder == dividend);
assert(quotient == operator_quotient);
operator_remainder := dividend - (operator_quotient * divisor);
assert(remainder == operator_remainder);
assert(divisor == x);
assert(quotient == y);
assert(remainder == r);
if quotient == .{}
assert(remainder == dividend);
}
}
}
}

File diff suppressed because it is too large Load Diff

1309
modules/Basic/Print.jai Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,295 @@
string_view :: inline (s: string, start_index: s64, view_count: s64) -> string {
assert(view_count >= 0 && start_index >= 0);
if view_count < 0 || start_index < 0 || start_index >= s.count return "";
new_count := view_count;
if start_index + view_count > s.count {
new_count = s.count - start_index;
}
return { new_count, s.data + start_index };
}
to_string :: inline (s: *u8, length: s64) -> string { // We should probably deprecate this in favor of your using string.{...}?
if !s return "";
result: string = ---;
result.count = length;
result.data = s;
return result;
}
// Intended for C strings in fixed-size arrays that may or may not be zero-terminated.
// It stops at the first zero in c_string.data but does not exceed c_string.count.
to_string :: (c_string: [] u8) -> string {
if c_string.count == 0 return "";
result: string = ---;
result.data = c_string.data;
length := 0;
while c_string[length] && length < c_string.count {
length += 1;
}
result.count = length;
return result;
}
// The following functions:
//
// to_string :: inline (s: *u8) -> string;
// c_style_strlen :: (x: *u8) -> s64;
//
// are in Runtime_Support.jai and can be called from anywhere.
// Allocates memory.
to_c_string :: (s: string) -> *u8 {
result : *u8 = alloc(s.count + 1);
memcpy(result, s.data, s.count);
result[s.count] = 0;
return result;
}
// Note: Right now when code is generated, c + A - a
// doesn't get constexpr evaluation evoked on the A - a
// because it parses into the AST as (c + A) - a, and we
// do not do tree rearrangement for constexpr evaluation.
// (And I am not sure that we should). -jblow, 25 April 2018.
// Maybe to_upper, to_lower, etc should not even be in Basic... -jblow
// These are locale indepdent, only support english characters, and do not support utf8.
to_upper :: (c: u8) -> u8 {
if c >= #char "a" && c <= #char "z" return c + #char "A" - #char "a";
else return c;
}
to_lower :: (c: u8) -> u8 {
if (c >= #char "A" && c <= #char "Z") return c + #char "a" - #char "A";
else return c;
}
is_alpha :: (c: u8) -> bool {
return (c >= #char "a" && c <= #char "z") || (c >= #char "A" && c <= #char "Z");
}
is_digit :: (c: u8) -> bool {
return c >= #char "0" && c <= #char "9";
}
is_alnum :: (c: u8) -> bool { // This also includes underscore!
return is_alpha(c) || is_digit(c) || c == #char "_";
}
is_space :: (c: u8) -> bool {
return c == #char " " || c == #char "\t" || c == #char "\r" || c == #char "\n";
}
is_print :: (c: u8) -> bool {
// True for any character from space to tilde (DEL not included).
return c >= 0x20 && c < 0x7F;
}
advance :: inline (s: *string, amount: s64 = 1) {
assert(amount >= 0);
assert(s.count >= amount);
s.count -= amount;
s.data += amount;
}
advance :: inline (s: string, amount: s64 = 1) -> string {
assert(amount >= 0);
assert(s.count >= amount);
t: string = ---;
t.count = s.count - amount;
t.data = s.data + amount;
return t;
}
temp_c_string :: (s: string) -> *u8 {
result : *u8 = temporary_alloc(s.count + 1);
memcpy(result, s.data, s.count);
result[s.count] = 0;
return result;
}
copy_temporary_string :: inline (s: string) -> string {
return copy_string(s,, temp);
}
copy_string :: (s: string) -> string {
if !s.count return "";
copy := alloc_string(s.count);
if !copy.data return "";
memcpy(copy.data, s.data, s.count);
return copy;
}
/*
@Bug: We want to have string_to_float be parameterized,
but it doesn't quite work consistently for some reason.
In both LLVM and x64, the procedures appear to return
0 sometimes (but not always, depending on what is going on
in the calling procedure). -jblow, 19 February 2017
Test code:
f1 := string_to_float("42.6");
print("f1 is type %, value %\n", type_of(f1), f1);
f2 := string_to_float64("42.6");
print("f2 is type %, value %\n", type_of(f2), f2);
f3 := string_to_float_old("42.6");
print("f3 is type %, value %\n", type_of(f3), f3);
string_to_float :: #bake_constants string_to_float_type_T (T = float);
string_to_float64 :: #bake_constants string_to_float_type_T (T = float64);
string_to_float_type_T :: (_arg: string) -> result: $T, success: bool, remainder: string {
arg := eat_spaces(_arg);
if !arg {
return 0, false, arg;
}
found_decimal := false;
// @Temporary:
// See how much of the string is a digit, +, -, ., or e.
limit := arg.count;
for 0..arg.count-1 {
c := arg[it];
if (c >= #char "0") && (c <= #char "9") {
// We are good.
} else if c == #char "." {
// We are good.
} else if (c == #char "-") || (c == #char "+") || (c == #char "e") {
// We are good.
} else {
limit = it;
break;
}
}
substring := arg;
substring.count = limit;
remainder := arg;
advance(*remainder, limit);
// @Temporary @Speed @Ugh
cs := temp_c_string(substring);
result := cast(T) atof(cs);
return result, true, remainder;
}
*/
//
// @Cleanup! We would like to just do:
//
// to_integer :: #bake_arguments string_to_int(base=10);
//
// But this causes dead code elimination to think string_to_int is live,
// even if it's not. So it would be good to come up with a way to make
// this work better! In the meantime...
//
to_integer :: (s: string) -> result: int, success: bool, remainder: string {
result, success, remainder := string_to_int(s);
return result, success, remainder;
}
string_to_int :: (t: string, base := 10, $T := int) -> result: T, success: bool, remainder: string {
//
// This string_to_int might be a little slower than max speed because we multiply by 'sign' for
// each digit. Our original approach did not do this, but rather we just accumulated into a
// positive value and then negated at the end if we saw a -. But this does not let us parse the
// maximum negative number without overflowing the positive number, which works, *if* you are
// not doing arithmetic bounds checks. We could have done #no_abc on this procedure, but
// I decided it's worthwhile to just try to preserve the ability to report genuine overflows,
// so I did 'the clean version' here. -jblow, 4 July 2023
//
assert(base == 16 || base <= 10);
s := eat_spaces(t);
if !s return 0, false, "";
sign : T = 1;
#if #run type_info(T).signed {
if s[0] == #char "-" {
sign = -1;
advance(*s, 1);
s = eat_spaces(s);
} else if s[0] == #char "+" {
advance(*s, 1);
s = eat_spaces(s);
}
}
sum: T = 0;
cursor := 0;
// @Incomplete: We should probably check for overflows…
if base == 16 {
while cursor < s.count {
c := s[cursor];
value: u8 = ---;
if is_digit(c) {
value = c - #char "0";
} else if (c >= #char "a") && (c <= #char "f") {
value = c - #char "a" + 10;
} else if (c >= #char "A") && (c <= #char "F") {
value = c - #char "A" + 10;
} else {
break;
}
sum *= cast(T) base;
sum += sign * cast(T)value;
cursor += 1;
}
} else {
while cursor < s.count {
c := s[cursor];
if !is_digit(c) break;
digit := c - #char "0";
if digit >= base break;
sum *= cast(T) base;
sum += sign*cast(T)digit;
cursor += 1;
}
}
success := (cursor != 0);
advance(*s, cursor);
return sum, success, s;
}
eat_spaces :: (_s: string) -> string {
s := _s;
while s {
if (s[0] != #char " ") && (s[0] != 9) break; // 9 is horizontal tab.
advance(*s, 1);
}
return s;
}

View File

@ -0,0 +1,345 @@
//
// String_Builder guarantees that buffers do not move after they have
// been allocated. Thus you can take pointers to positions inside the
// builder and know they have not changed.
//
//
// builder_to_string() is the function that people call at the end
// to get the result; we have made this free the memory by default,
// to avoid an extra step where you have to free things.
//
//
// You can copy the String_Builder by value and the copy will
// function just fine. (But it will also own all the buffers that were
// owned by the old copy, so make sure to stop using the old copy,
// or chaos will ensue.)
//
STRING_BUILDER_BUFFER_SIZE :: 4096 - size_of(String_Builder.Buffer);
String_Builder :: struct {
Buffer :: struct {
count: s64;
//
// The data pointer is implicit and points to the memory after the buffer itself
// (see "get_buffer_data()"). Otherwise we wouldn't be able to copy the String_Builder
// without having to adjust pointers.
//
// We could also use a relative pointer, but since it will always point after buffer,
// there's no need to store it explicitly. -rluba, 2021-12-15
//
allocated: s64;
#if ENABLE_ASSERT ensured_count: s64; // Used for error-checking on ensure_contiguous_space.
next: *Buffer;
}
initialized := false;
failed := false; // If we ever fail to allocate enough memory, this gets set to true ... we can't be sure the entire string has been added.
subsequent_buffer_size := -1;
allocator: Allocator;
current_buffer: *Buffer; // if current_buffer == null, it means use initial_bytes as a base buffer. We don't point it at initial_bytes, so that you can copy a String_Builder in memory and it's still valid.
initial_bytes: [STRING_BUILDER_BUFFER_SIZE] u8; // We don't dynamically allocate if we only need one buffer's worth of data.
}
init_string_builder :: (using builder: *String_Builder, buffer_size := -1) {
if (!allocator.proc) {
allocator = context.allocator;
}
subsequent_buffer_size = ifx buffer_size > 0 then buffer_size else STRING_BUILDER_BUFFER_SIZE;
base_buffer := get_base_buffer(builder);
base_buffer.count = 0;
base_buffer.allocated = initial_bytes.count - size_of(Buffer);
base_buffer.next = null;
current_buffer = null;
initialized = true;
}
free_buffers :: (using builder: *String_Builder) { // free_buffers will be deprecated pretty soon, and the only entry point to resetting a String_Builder will be reset(). -jblow, 1 June 2022
base_buffer := get_base_buffer(builder);
buffer := base_buffer.next;
while buffer {
next := buffer.next;
free(buffer,, allocator=allocator);
buffer = next;
}
base_buffer.next = null; // Just in case we call this twice.
}
reset :: (using builder: *String_Builder) {
free_buffers(builder);
base_buffer := get_base_buffer(builder);
base_buffer.count = 0;
current_buffer = null;
}
ensure_contiguous_space :: (using builder: *String_Builder, bytes: s64) -> pointer: *void { // 'pointer' points to the base of the space, or is null if we failed.
available := ensure_contiguous_space_and_return_available_range(builder, bytes);
if !available.count then return null;
#if ENABLE_ASSERT {
buffer := get_current_buffer(builder);
buffer.ensured_count = buffer.count + bytes;
}
return available.data;
}
ensure_contiguous_space_and_return_available_range :: (using builder: *String_Builder, minimum_bytes := 1) -> [] u8 {
if !initialized init_string_builder(builder);
buffer := get_current_buffer(builder);
if minimum_bytes > buffer.allocated then return .[]; // Nothing we can do there, bro; it's bigger than our buffers!
available := buffer.allocated - buffer.count;
if available < minimum_bytes {
success := expand(builder);
if !success then return .[];
buffer = get_current_buffer(builder);
}
#if ENABLE_ASSERT buffer.ensured_count = buffer.allocated;
result: [] u8;
result.data = get_buffer_data(buffer) + buffer.count;
result.count = buffer.allocated - buffer.count;
return result;
}
advance_through_ensured_space :: (using builder: *String_Builder, bytes: s64) {
// If you ensure space and use the pointer you get back, you might write data
// directly into that pointer, but then want to advance it without packing stuff
// manually into the builder. That's what this is for.
buffer := get_current_buffer(builder);
length_max := buffer.allocated - buffer.count;
assert(length_max >= bytes);
// In order to check whether you didn't use ensure_contiguous_space wrong, we
// don't only check whether you happen to have enough space in the buffer
// (above), but we check whether you guaranteed the space correctly, which
// doesn't depend on chance of where we are in the buffer.
#if ENABLE_ASSERT assert(buffer.count + bytes <= buffer.ensured_count);
buffer.count += bytes;
assert(initialized); // There
}
append :: (using builder: *String_Builder, s: *u8, length: s64) {
if !initialized init_string_builder(builder);
while length > 0 {
buffer := get_current_buffer(builder);
length_max := buffer.allocated - buffer.count;
if length_max <= 0 {
success := expand(builder);
if !success {
failed = true;
return;
}
// expand() changes current_buffer, so let's reacquire!
buffer = current_buffer;
assert(buffer != null);
length_max = buffer.allocated - buffer.count; // Avoid a recursion doing no work that would happen if we left length_max == 0. (It should never be < 0 unless something very bad has happened).
assert(length_max > 0);
}
to_copy := min(length, length_max);
if length > 0 assert(to_copy >= 0);
memcpy(get_buffer_data(buffer) + buffer.count, s, to_copy);
buffer.count += to_copy;
length -= to_copy;
s += to_copy;
}
}
append :: inline (builder: *String_Builder, s: string) {
append(builder, s.data, s.count);
}
append :: inline (builder: *String_Builder, byte: u8) {
local_byte := byte; // So we can take the address, yay.
append(builder, *local_byte, 1);
}
append_by_pointer :: inline (builder: *String_Builder, pointer: *$T) {
append(builder, cast(*u8) pointer, size_of(T));
}
builder_string_length :: (using builder: *String_Builder) -> int {
if !allocator.proc return 0;
buffer := get_base_buffer(builder);
bytes := 0;
while buffer {
bytes += buffer.count;
buffer = buffer.next;
}
return bytes;
}
builder_to_string :: (builder: *String_Builder, extra_bytes_to_prepend := 0, do_reset := true) -> string {
// 'extra_bytes_to_prepend' gives you extra space at the beginning, so you can
// do something like put in a length that you didn't know when you were
// writing out the data. This may not be worth the extra complexity, though;
// for heavier-duty use you probably want something like the String_Builder bucket stealing
// that is in the compiler's source but hasn't yet made it into this port yet.
// -jblow, 30 September 2020
if !builder.initialized init_string_builder(builder);
count := builder_string_length(builder);
if !count && !extra_bytes_to_prepend {
return "";
}
result: string = ---;
result.data = alloc(count + extra_bytes_to_prepend);
result.count = count + extra_bytes_to_prepend;
data := result.data + extra_bytes_to_prepend;
buffer := get_base_buffer(builder);
while buffer {
memcpy(data, get_buffer_data(buffer), buffer.count);
data += buffer.count;
buffer = buffer.next;
}
if do_reset reset(builder); // @Speed: Could free buffers inline with the bucket traversal above. Code would be less clear.
return result;
}
write_builder :: (using builder: *String_Builder, do_reset := true, to_standard_error := false) -> s64 {
//
// Future expanded functionality: We can make a macro
// that calls its argument code with a string representing
// the contents of each bucket. Then you can do whatever
// operation you want.
// -jblow, 19 July 2019
//
if !allocator.proc return 0;
buffer := get_base_buffer(builder);
written := 0;
while buffer {
s: string = ---;
s.data = get_buffer_data(buffer);
s.count = buffer.count;
write_string(s, to_standard_error);
written += s.count;
buffer = buffer.next;
}
if do_reset reset(builder);
return written;
}
consume_u8_and_length :: (input: *string, s: *u8, length: s64) -> bool { // Looks a lot like append_u8_and_length.
if length < 0 return false; // Error!
if length > input.count return false; // Error!
if length == 0 return true;
memcpy(s, input.data, length);
input.count -= length;
input.data += length;
return true;
}
get_current_buffer :: inline (using builder: *String_Builder) -> *String_Builder.Buffer {
if current_buffer return current_buffer;
assert(builder.allocator.proc != null);
return get_base_buffer(builder);
}
get_base_buffer :: inline (using builder: *String_Builder) -> *String_Builder.Buffer {
return cast(*Buffer) initial_bytes.data;
}
get_buffer_data :: inline (using buffer: *String_Builder.Buffer) -> *u8 {
return (cast (*u8) buffer) + size_of(String_Builder.Buffer);
}
simple_memcpy :: inline (using builder: *String_Builder, x: $T) {
// Don't call this unless you know the buffer has been initialized and there is enough space in the current buffer!
// Otherwise you will corrupt memory and be sad.
// This is included here just as a utility routine you can call
// when writing serializers and whatnot.
using buffer := get_current_buffer(builder);
assert(size_of(T) <= allocated - count);
memcpy(get_buffer_data(buffer) + count, *x, size_of(T));
count += size_of(T);
}
expand :: (using builder: *String_Builder) -> bool {
assert (allocator.proc != null); // This is initialized in init_string_builder.
subsequent := ifx subsequent_buffer_size > 0 then subsequent_buffer_size else STRING_BUILDER_BUFFER_SIZE;
bytes := cast(*u8) alloc(size_of(Buffer) + subsequent,, allocator);
if !bytes return false; // Failed!
buffer := cast(*Buffer) bytes;
buffer.next = null;
buffer.count = 0;
buffer.allocated = subsequent;
old_buffer := get_current_buffer(builder);
old_buffer.next = buffer;
current_buffer = buffer;
return true;
}
// Move all data from "steal_from" to "builder", taking ownership of all dynamically allocated buffers in the process.
append_and_steal_buffers :: (builder: *String_Builder, steal_from: *String_Builder) {
buffer := get_base_buffer(steal_from);
if !buffer.count return;
// We need to copy the base buffer because it’s part of the other builder’s struct
// so we can’t take ownership of it:
append(builder, get_buffer_data(buffer), buffer.count);
assert(builder.allocator.proc == steal_from.allocator.proc);
assert(builder.allocator.data == steal_from.allocator.data);
// Take ownership of the rest of the buffer chain:
our_end := get_current_buffer(builder);
our_end.next = buffer.next;
// Reset the victim’s base buffer
buffer.count = 0;
buffer.next = null;
if steal_from.current_buffer {
builder.current_buffer = steal_from.current_buffer; // Use the victim’s final buffer as our current buffer.
steal_from.current_buffer = null; // Reset the victim to use the base buffer again
}
}

View File

@ -0,0 +1,252 @@
Overflow_Page :: struct {
next: *Overflow_Page;
allocator: Allocator;
size: s64;
}
Temporary_Storage_Paged_Alloc :: struct {
#as using base: Temporary_Storage;
#overlay (implementation_data) using derived: struct {
overflow_allocator : Allocator;
overflow_pages: *Overflow_Page;
original_data: *u8; // Data to restore after clearing overflow pages. @Simplify: Maybe could be an Overflow_Page, but maybe we want to be able to assert on overflow_pages==null to ensure performance.
original_size: s64;
current_page_bytes_occupied: s64;
}
}
Temporary_Storage_State :: struct {
top_overflow_page: *Overflow_Page;
current_page_bytes_occupied: s64;
total_bytes_occupied: s64;
}
temporary_allocator_proc :: (mode: Allocator_Mode, requested_size: s64, old_size: s64, old_memory: *void, allocator_data: *void) -> *void {
if #complete mode == {
case .FREE;
return null;
case .RESIZE;
real_old_size := (old_size + 7) & ~7; // We know old_size was padded
if requested_size <= real_old_size return old_memory;
#through;
case .ALLOCATE;
// Pad to 8-byte alignment.
size := (requested_size + 7) & ~7;
ts := cast(*Temporary_Storage_Paged_Alloc) context.temporary_storage;
// If someone gave us a context with no temporary_storage, fall back to context.allocator.
if !ts {
a := context.allocator;
if a.proc == temporary_allocator_proc {
// context.allocator is temporary_allocator, so, fall back to default_allocator.
a.proc = Context_Base.default_allocator.proc;
a.data = null;
}
return a.proc(mode, requested_size, old_size, old_memory, a.data);
}
if size > ts.size - ts.current_page_bytes_occupied {
success := add_new_page(ts, size);
if !success return null;
}
result := ts.data + ts.current_page_bytes_occupied;
if result && (mode == .RESIZE) {
memcpy(result, old_memory, min(old_size, requested_size));
}
ts.current_page_bytes_occupied += cast(s32) size;
ts.total_bytes_occupied += cast(s32) size;
ts.high_water_mark = max(ts.high_water_mark, ts.total_bytes_occupied);
return result;
case .STARTUP; #through;
case .SHUTDOWN;
return null;
case .THREAD_START; #through;
case .THREAD_STOP;
assert(false, "Multithreaded access is not supported by temporary_allocator_proc.\n");
return null;
case .CREATE_HEAP; #through;
case .DESTROY_HEAP;
assert(false, "Create/Destroy heap are not supported by temporary_allocator_proc.\n");
return null;
case .IS_THIS_YOURS;
ts := cast(*Temporary_Storage_Paged_Alloc) context.temporary_storage;
if !ts return null;
if (old_memory >= ts.data) && (old_memory < ts.data + ts.size) return cast(*void) true;
if (old_memory >= ts.original_data) && (old_memory < ts.original_data + ts.original_size) return cast(*void) true;
page := ts.overflow_pages;
while page {
if (old_memory >= page) && (old_memory < cast(*void)page + page.size + ALIGNED_OVERFLOW_PAGE_ALLOCATION) return cast(*void) true;
page = page.next;
}
// Not found!
return null;
case .CAPS;
// if old_memory { (.*) cast(*string)old_memory = "modules/Basic temporary_allocator_proc"; }
if old_memory { (cast(*string)old_memory).* = "modules/Basic temporary_allocator_proc"; }
return cast(*void)(Allocator_Caps.HINT_I_AM_PER_FRAME_TEMPORARY_STORAGE);
}
}
auto_release_temp :: (loc := #caller_location) #expand {
__mark := get_temporary_storage_mark();
`defer set_temporary_storage_mark(__mark, loc);
}
get_temporary_storage_mark :: inline () -> Temporary_Storage_State {
ts := cast(*Temporary_Storage_Paged_Alloc) context.temporary_storage;
if !ts return .{}; // Just to have something to return!
state: Temporary_Storage_State = ---;
state.current_page_bytes_occupied = ts.current_page_bytes_occupied;
state.total_bytes_occupied = ts.total_bytes_occupied;
state.top_overflow_page = ts.overflow_pages;
return state;
}
// Typically used as follows:
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// mark := get_temporary_storage_mark();
// defer set_temporary_storage_mark(mark);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// You can also use the `auto_release_temp` macro.
set_temporary_storage_mark :: inline (state: Temporary_Storage_State, loc := #caller_location) {
ts := cast(*Temporary_Storage_Paged_Alloc) context.temporary_storage;
if !ts return;
free_pages_down_to(ts, state.top_overflow_page);
ts.last_set_mark_location = loc;
if state.top_overflow_page {
page := state.top_overflow_page;
ts.data = (cast(*u8) page) + ALIGNED_OVERFLOW_PAGE_ALLOCATION;
ts.size = page.size;
assert(page.size >= 0);
} else {
ts.data = ts.original_data;
ts.size = ts.original_size;
assert(ts.overflow_pages == null);
}
ts.current_page_bytes_occupied = state.current_page_bytes_occupied;
ts.total_bytes_occupied = state.total_bytes_occupied;
#if TEMP_ALLOCATOR_OVERWRITE_MEMORY {
memset(cast(*u8) ts.data + ts.current_page_bytes_occupied, SET_STORAGE_MARK_STAMP, ts.size - ts.current_page_bytes_occupied);
}
}
reset_temporary_storage :: (_ts: *Temporary_Storage) {
ts := cast(*Temporary_Storage_Paged_Alloc)_ts;
ts.data = ts.original_data;
ts.size = ts.original_size;
ts.current_page_bytes_occupied = 0;
ts.total_bytes_occupied = 0;
ts.high_water_mark = 0;
// @Speed: Maybe we should keep the allocated pages on the side,
// rather than freeing them every frame. It probably would not
// add much complexity here.
free_pages_down_to(ts, null);
#if TEMP_ALLOCATOR_OVERWRITE_MEMORY {
memset(ts.data, RESET_STORAGE_STAMP, ts.size);
}
}
free_pages_down_to :: (ts: *Temporary_Storage_Paged_Alloc, page: *Overflow_Page) {
cursor := ts.overflow_pages;
while cursor != page {
if !cursor {
log_error("Attempt to free down to page %, but that page was not found! This is probably an error in using set_temporary_storage_mark, or a general memory corruption.\n", page);
ts.overflow_pages = null;
return;
}
next := cursor.next;
allocator := cursor.allocator;
#if TEMP_ALLOCATOR_OVERWRITE_MEMORY {
size_including_header := get_size_including_overflow_page_header(cursor.size);
memset(cursor, FREE_PAGE_STAMP, size_including_header);
}
free(cursor,, allocator=allocator);
cursor = next;
}
ts.overflow_pages = page;
}
ALIGNED_OVERFLOW_PAGE_ALLOCATION :: 32;
get_size_including_overflow_page_header :: (data_size: s64) -> s64 {
size := data_size + ALIGNED_OVERFLOW_PAGE_ALLOCATION;
// Pad to 8-byte alignment.
size = (size + 7) & ~7;
return size;
}
add_new_page :: (ts: *Temporary_Storage_Paged_Alloc, minimum_size: s64) -> bool {
#assert(size_of(Overflow_Page) <= ALIGNED_OVERFLOW_PAGE_ALLOCATION);
data_size : s64 = ts.original_size - ALIGNED_OVERFLOW_PAGE_ALLOCATION;
if data_size < minimum_size data_size = minimum_size;
size_including_header := get_size_including_overflow_page_header(data_size);
// It's legal for overflow_allocator to be null, in which case
// we do whatever alloc does.
allocator := ts.overflow_allocator;
if !allocator.proc {
// We don't know what allocator might be pushed onto the context,
// so, fall back to __default_allocator.
allocator = Context_Base.default_allocator;
}
memory := alloc(size_including_header,, allocator=allocator);
if !memory {
log_error("Failed to allocate % bytes for a Temporary_Storage overflow page.\n", size_including_header);
return false; // This is going to be bad!
}
if ts.overflow_pages == null {
// Save out the original size and data,
// so we can restore them later.
ts.original_data = ts.data;
ts.original_size = ts.size;
}
page := cast(*Overflow_Page) memory;
page.next = ts.overflow_pages;
page.allocator = allocator;
page.size = data_size;
ts.overflow_pages = page;
ts.data = memory + ALIGNED_OVERFLOW_PAGE_ALLOCATION;
ts.size = data_size;
ts.current_page_bytes_occupied = 0;
assert(ts.size >= 0);
return true;
}

View File

@ -0,0 +1,257 @@
Overflow_Page :: struct {
next: *Overflow_Page;
allocator: Allocator;
size: s64;
}
Temporary_Storage_Paged_Alloc :: struct {
#as using base: Temporary_Storage;
#overlay (implementation_data) using derived: struct {
overflow_allocator : Allocator;
overflow_pages: *Overflow_Page;
original_data: *u8; // Data to restore after clearing overflow pages. @Simplify: Maybe could be an Overflow_Page, but maybe we want to be able to assert on overflow_pages==null to ensure performance.
original_size: s64;
current_page_bytes_occupied: s64;
}
}
Temporary_Storage_State :: struct {
top_overflow_page: *Overflow_Page;
current_page_bytes_occupied: s64;
total_bytes_occupied: s64;
}
temporary_allocator_proc :: (mode: Allocator_Mode, requested_size: s64, old_size: s64, old_memory: *void, allocator_data: *void) -> *void {
if #complete mode == {
case .FREE;
return null;
case .RESIZE;
real_old_size := (old_size + 7) & ~7; // We know old_size was padded
if requested_size <= real_old_size return old_memory;
#through;
case .ALLOCATE;
// Pad to 8-byte alignment.
size := (requested_size + 7) & ~7;
ts := cast(*Temporary_Storage_Paged_Alloc) context.temporary_storage;
// If someone gave us a context with no temporary_storage, fall back to context.allocator.
if !ts {
a := context.allocator;
if a.proc == temporary_allocator_proc {
// context.allocator is temporary_allocator, so, fall back to default_allocator.
a = Context_Base.default_allocator;
}
return a.proc(mode, requested_size, old_size, old_memory, a.data);
}
if size > ts.size - ts.current_page_bytes_occupied {
success := add_new_page(ts, size);
if !success return null;
}
result := ts.data + ts.current_page_bytes_occupied;
if result && (mode == .RESIZE) {
memcpy(result, old_memory, min(old_size, requested_size));
}
ts.current_page_bytes_occupied += size;
ts.total_bytes_occupied += size;
ts.high_water_mark = max(ts.high_water_mark, ts.total_bytes_occupied);
return result;
case .STARTUP; #through;
case .SHUTDOWN;
return null;
case .THREAD_START; #through;
case .THREAD_STOP;
assert(false, "Multithreaded access is not supported by temporary_allocator_proc.\n");
return null;
case .CREATE_HEAP; #through;
case .DESTROY_HEAP;
assert(false, "Create/Destroy heap are not supported by temporary_allocator_proc.\n");
return null;
case .IS_THIS_YOURS;
ts := cast(*Temporary_Storage_Paged_Alloc) context.temporary_storage;
if !ts return null;
if (old_memory >= ts.data) && (old_memory < ts.data + ts.size) return cast(*void) true;
if (old_memory >= ts.original_data) && (old_memory < ts.original_data + ts.original_size) return cast(*void) true;
page := ts.overflow_pages;
while page {
if (old_memory >= page) && (old_memory < cast(*void)page + page.size + ALIGNED_OVERFLOW_PAGE_ALLOCATION) return cast(*void) true;
page = page.next;
}
// Not found!
return null;
case .CAPS;
// if old_memory { (.*) cast(*string)old_memory = "modules/Basic temporary_allocator_proc"; }
if old_memory { (cast(*string)old_memory).* = "modules/Basic temporary_allocator_proc"; }
return cast(*void)(Allocator_Caps.HINT_I_AM_PER_FRAME_TEMPORARY_STORAGE);
}
}
auto_release_temp :: (loc := #caller_location) #expand {
__mark := get_temporary_storage_mark();
`defer set_temporary_storage_mark(__mark, loc);
}
get_temporary_storage_mark :: inline () -> Temporary_Storage_State {
ts := cast(*Temporary_Storage_Paged_Alloc) context.temporary_storage;
if !ts return .{}; // Just to have something to return!
state: Temporary_Storage_State = ---;
state.current_page_bytes_occupied = ts.current_page_bytes_occupied;
state.total_bytes_occupied = ts.total_bytes_occupied;
state.top_overflow_page = ts.overflow_pages;
return state;
}
// Typically used as follows:
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// mark := get_temporary_storage_mark();
// defer set_temporary_storage_mark(mark);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// You can also use the `auto_release_temp` macro.
set_temporary_storage_mark :: inline (state: Temporary_Storage_State, loc := #caller_location) {
ts := cast(*Temporary_Storage_Paged_Alloc) context.temporary_storage;
if !ts return;
ts.last_set_mark_location = loc;
if state.top_overflow_page {
free_pages_down_to(ts, state.top_overflow_page);
page := state.top_overflow_page;
ts.data = (cast(*u8) page) + ALIGNED_OVERFLOW_PAGE_ALLOCATION;
ts.size = page.size;
assert(page.size >= 0);
} else {
if ts.overflow_pages {
free_pages_down_to(ts, null);
ts.data = ts.original_data;
ts.size = ts.original_size;
}
assert(ts.overflow_pages == null);
}
ts.current_page_bytes_occupied = state.current_page_bytes_occupied;
ts.total_bytes_occupied = state.total_bytes_occupied;
#if TEMP_ALLOCATOR_OVERWRITE_MEMORY {
memset(cast(*u8) ts.data + ts.current_page_bytes_occupied, SET_STORAGE_MARK_STAMP, ts.size - ts.current_page_bytes_occupied);
}
}
reset_temporary_storage :: (_ts: *Temporary_Storage) {
ts := cast(*Temporary_Storage_Paged_Alloc)_ts;
if ts.overflow_pages {
ts.data = ts.original_data;
ts.size = ts.original_size;
}
ts.current_page_bytes_occupied = 0;
ts.total_bytes_occupied = 0;
ts.high_water_mark = 0;
// @Speed: Maybe we should keep the allocated pages on the side,
// rather than freeing them every frame. It probably would not
// add much complexity here.
free_pages_down_to(ts, null);
#if TEMP_ALLOCATOR_OVERWRITE_MEMORY {
memset(ts.data, RESET_STORAGE_STAMP, ts.size);
}
}
free_pages_down_to :: (ts: *Temporary_Storage_Paged_Alloc, page: *Overflow_Page) {
cursor := ts.overflow_pages;
while cursor != page {
if !cursor {
log_error("Attempt to free down to page %, but that page was not found! This is probably an error in using set_temporary_storage_mark, or a general memory corruption.\n", page);
ts.overflow_pages = null;
return;
}
next := cursor.next;
allocator := cursor.allocator;
#if TEMP_ALLOCATOR_OVERWRITE_MEMORY {
size_including_header := get_size_including_overflow_page_header(cursor.size);
memset(cursor, FREE_PAGE_STAMP, size_including_header);
}
free(cursor,, allocator=allocator);
cursor = next;
}
ts.overflow_pages = page;
}
ALIGNED_OVERFLOW_PAGE_ALLOCATION :: 32;
get_size_including_overflow_page_header :: (data_size: s64) -> s64 {
size := data_size + ALIGNED_OVERFLOW_PAGE_ALLOCATION;
// Pad to 8-byte alignment.
size = (size + 7) & ~7;
return size;
}
add_new_page :: (ts: *Temporary_Storage_Paged_Alloc, minimum_size: s64) -> bool {
#assert,stallable(size_of(Overflow_Page) <= ALIGNED_OVERFLOW_PAGE_ALLOCATION);
data_size : s64 = ts.original_size - ALIGNED_OVERFLOW_PAGE_ALLOCATION;
if data_size < minimum_size data_size = minimum_size;
size_including_header := get_size_including_overflow_page_header(data_size);
// It's legal for overflow_allocator to be null, in which case
// we do whatever alloc does.
allocator := ts.overflow_allocator;
if !allocator.proc {
// We don't know what allocator might be pushed onto the context,
// so, fall back to default_allocator.
allocator = Context_Base.default_allocator;
}
memory := alloc(size_including_header,, allocator=allocator);
if !memory {
log_error("Failed to allocate % bytes for a Temporary_Storage overflow page.\n", size_including_header);
return false; // This is going to be bad!
}
if ts.overflow_pages == null {
// Save out the original size and data,
// so we can restore them later.
ts.original_data = ts.data;
ts.original_size = ts.size;
}
page := cast(*Overflow_Page) memory;
page.next = ts.overflow_pages;
page.allocator = allocator;
page.size = data_size;
ts.overflow_pages = page;
ts.data = memory + ALIGNED_OVERFLOW_PAGE_ALLOCATION;
ts.size = data_size;
ts.current_page_bytes_occupied = 0;
assert(ts.size >= 0);
return true;
}

View File

@ -0,0 +1,369 @@
//
// This code communicates the collected information about memory allocations
// to an external visualization client like examples/codex_view.
//
vis_init :: () {
// We allow you to start the visualization client after the program is running, so
// if we have not connected, we keep trying. MILLISECONDS_PER_VIS_INIT controls
// how often we retry.
now := current_time_monotonic();
if last_update_time != APOLLO_TIME_INVALID {
ms := to_milliseconds(now - last_update_time);
if ms < MILLISECONDS_PER_VIS_INIT return;
}
last_update_time = now;
// It's the first try, or it's been long enough since the last try.
// So, let's try to connect.
success, channel := writer_connect(MEMORY_VISUALIZATION_CHANNEL_NAME, name_not_found_is_quiet=true);
if !success {
if !logged_retry {
log("Unable to connect to a memory visualization client. Will retry in case one is started.\n");
logged_retry = true;
}
return;
}
vis_initted = true;
bytes_total := size_of(Memory_Debugger_Version_Info);
message_success, message := writer_reserve_message(*channel, bytes_total);
if !message_success {
log_error("In vis_init(), we were unable to reserve the initial message on the channel. Disabling.\n");
return;
}
info := cast(*Memory_Debugger_Version_Info) message.user_write_pointer;
info.tag = .RESET_AND_TELL_VERSION;
info.version = VIS_PROTOCOL_VERSION;
info.default_allocator_proc = runtime_support_default_allocator_proc;
writer_commit_message(*channel, message);
vis_channel = channel;
vis_enabled = true;
}
maybe_add_string :: (s: string) {
found, value := table_find(*table_sent_strings, s);
if !found {
array_add(*to_send_strings, s);
index := cast(s32) table_sent_strings.count;
table_set(*table_sent_strings, s, index);
}
}
vis_update_if_it_has_been_long_enough :: () {
if !vis_initted vis_init();
if !vis_enabled return;
if did_user_level_per_frame_update return; // Once the user calls a manual update, we expect that is the way it should go henceforth.
now := current_time_monotonic();
if last_update_time == APOLLO_TIME_INVALID {
last_update_time = now;
} else {
delta := now - last_update_time;
ms := to_milliseconds(delta);
if ms >= MILLISECONDS_PER_VIS_UPDATE {
vis_update();
last_update_time = now;
}
}
}
vis_update :: () {
//
// :InsideMemoryDebugger
//
// We set inside_memory_debugger so that we don't mess up the accounting that the user sees,
// and also, so that the report size stays stable while we are trying to transmit it.
//
context.inside_memory_debugger = true;
defer context.inside_memory_debugger = false;
// For each pending stack trace, see if we need to add its strings
// and Procedure_Infos.
for trace: pending_stack_traces {
for node: trace.nodes {
info := node.info;
found, value := table_find(*table_sent_procedure_infos, info);
if !found {
array_add(*to_send_procedure_infos, info);
table_set(*table_sent_procedure_infos, info, true);
maybe_add_string(info.name);
maybe_add_string(info.location.fully_pathed_filename);
}
}
array_add(*to_send_stack_traces, trace);
}
pending_stack_traces.count = 0;
// Send data for the new strings.
for s: to_send_strings {
count := cast,trunc(s32) s.count;
if count < 0 count = 0;
bytes_total := size_of(String_Info) + count;
success, message := writer_reserve_message(*vis_channel, bytes_total);
if !success break; // We need to wait until later...!
data := message.user_write_pointer;
info := cast(*String_Info) data;
data += size_of(String_Info);
found, index := table_find(*table_sent_strings, s);
assert(found);
info.tag = .ADD_STRING;
info.index = cast(String_Index) index;
info.count = count;
memcpy(data, s.data, count);
writer_commit_message(*vis_channel, message);
remove s;
}
// Send data for the new Procedure_Infos.
for info: to_send_procedure_infos {
bytes_total := size_of(Packed_Stack_Trace_Procedure_Info);
success, message := writer_reserve_message(*vis_channel, bytes_total);
if !success break; // We need to wait until later...!
p := cast(*Packed_Stack_Trace_Procedure_Info) message.user_write_pointer;
name_found, name_index := table_find(*table_sent_strings, info.name);
assert(name_found);
filename_found, filename_index := table_find(*table_sent_strings, info.location.fully_pathed_filename);
assert(filename_found);
p.tag = .ADD_PROCEDURE_INFO;
p.name = cast(String_Index) name_index;
p.filename = cast(String_Index) filename_index;
p.line_number = cast,trunc(s32) info.location.line_number;
p.character_number = cast,trunc(s32) info.location.character_number;
p.remote_address = info;
writer_commit_message(*vis_channel, message);
remove info;
}
// Now send data for the new stack traces.
for trace: to_send_stack_traces {
// @Incomplete: Names of these Stack_Trace_Nodes are in the wrong address space,
// so we can't draw them yet. We will probably send mappings for the strings
// so that the visualizer can put them in a hash table.
bytes := size_of(Single_Stack_Trace_Info) + trace.nodes.count * size_of(Stack_Trace_Node);
success, message := writer_reserve_message(*vis_channel, bytes);
if !success break; // We need to wait until later...!
// Write the ADD_STACK_TRACE command, the hash, and the address in our address space
// of this Packed_Stack_Trace (since the Leak_Summary uses that).
data := message.user_write_pointer;
info := cast(*Single_Stack_Trace_Info) data;
info.tag = .ADD_STACK_TRACE;
info.hash = trace.hash;
info.address = trace;
info.num_nodes = cast(s32) trace.nodes.count;
data += size_of(Single_Stack_Trace_Info);
memcpy(data, trace.nodes.data, trace.nodes.count * size_of(Stack_Trace_Node));
writer_commit_message(*vis_channel, message);
remove trace;
}
// Dump the current report.
// @Speed -- now the amount of time it takes to generate the report matters,
// since we are doing it during runtime, so we will want to
report := make_leak_report(for_visualizer=true); // @Incomplete: No way to set leak_report_options from here yet.
defer deinit(*report);
//
// :InsideMemoryDebugger
//
// make_leak_report sets this to false, assuming that the caller
// is not inside the memory debugger. Mayhap we should do this in a more-formal way,
// but for now we just set it back to true. (Keep in mind there is a 'defer' active
// that will set it to false, still, when we close the scope.)
//
context.inside_memory_debugger = true;
//
// When accessing allocs_this_frame and frees_this_frame, we must be holding address_table_mutex,
// since these could be modified by alloc(), free(), realloc() etc, which can be called on many threads.
// We would prefer not to be doing much work inside this mutex, *but* report.sorted_summaries could be
// quite large, so we copy the per-frame data (which is hopefully pretty small) so that we don't have to
// hold the mutex that whole time. There could be some other creative solutions to the problem; for example,
// we only need to know the byte total because we are sending a single message containing all the data,
// but if we broke the mutex-controlled data into a separate message, we could perhaps avoid some of
// this entanglement (but then you'd have to be guaranteed that the message target memory allocation is fast...)
//
bytes_total: int;
bytes_for_mutex_controlled_data: int;
num_allocs_this_frame: s32;
num_frees\_this_frame: s32;
{
lock(*address_table_mutex);
defer unlock(*address_table_mutex);
bytes_for_mutex_controlled_data := allocs_this_frame.count * size_of(Allocs_This_Frame) + frees_this_frame.count * size_of(Frees_This_Frame);
bytes_total = size_of(Summary_Info) + report.sorted_summaries.count * size_of(Packed_Summary) + bytes_for_mutex_controlled_data;
num_allocs_this_frame = cast(s32) allocs_this_frame.count;
num_frees\_this_frame = cast(s32) frees\_this_frame.count;
if mutex_controlled_data_copy.allocated < bytes_for_mutex_controlled_data {
// We have an array that just sits there and grows over time,
// never recycling the memory, so hopefully we do not do this
// array_reserve very often!
// :InsideMemoryDebugger
//
// This is one of the reasons we want inside_memory_debugger = true; this allocation
// would otherwise show up on stats.
push_allocator(Context_Base.default_allocator);
array_reserve(*mutex_controlled_data_copy, bytes_for_mutex_controlled_data);
}
mutex_controlled_data_copy.count = bytes_for_mutex_controlled_data;
data := mutex_controlled_data_copy.data;
for * allocs_this_frame {
p := cast(*Allocs_This_Frame) data;
p.* = it.*;
data += size_of(Allocs_This_Frame);
}
for * frees_this_frame {
p := cast(*Frees_This_Frame) data;
p.* = it.*;
data += size_of(Frees_This_Frame);
}
assert(data == mutex_controlled_data_copy.data + mutex_controlled_data_copy.count);
}
success, message := writer_reserve_message(*vis_channel, bytes_total);
if success {
data := message.user_write_pointer;
info := cast(*Summary_Info) data;
info.tag = .UPDATE_USAGE;
info.num_live = cast(s32) report.sorted_summaries.count;
info.num_allocs_this_frame = num_allocs_this_frame;
info.num_frees\_this_frame = num_frees_this_frame;
data += size_of(Summary_Info);
for report.sorted_summaries {
p := cast(*Packed_Summary) data;
p.count = it.count;
p.bytes = it.bytes;
p.allocations\ _since_last_visualizer_update = it.allocations\ _since_last_visualizer_update;
p.bytes_allocated_since_last_visualizer_update = it.bytes_allocated_since_last_visualizer_update;
p.alloc_site_trace = it.alloc_site_trace;
p.allocator_proc = it.allocator_proc;
if it.group_info {
p.grouped_from_this_many_stack_traces = cast(s32) it.group_info.sub_summaries.count;
p.root_nodes_in_common = cast(s32) it.group_info.root_nodes_in_common;
p.leaf_nodes_in_common = cast(s32) it.group_info.leaf_nodes_in_common;
} else {
p.grouped_from_this_many_stack_traces = 0;
p.root_nodes_in_common = 0;
p.leaf_nodes_in_common = 0;
}
data += size_of(Packed_Summary);
}
memcpy(data, mutex_controlled_data_copy.data, mutex_controlled_data_copy.count);
data += mutex_controlled_data_copy.count;
assert(data == message.user_write_pointer + bytes_total);
writer_commit_message(*vis_channel, message);
}
{
lock(*address_table_mutex);
defer unlock(*address_table_mutex);
table_reset(*allocs_this_frame); // Leave the memory of these arrays active so we don't keep allocating it.
table_reset(*frees_this_frame);
}
}
vis_fini :: () {
if !vis_enabled return;
writer_disconnect(*vis_channel);
vis_enabled = false;
}
memory_visualizer_per_frame_update :: () {
did_user_level_per_frame_update = true; // @Threadsafety... if we turn back on the long_enough thing, we have a race here.
if !vis_initted vis_init();
if !vis_enabled return;
// This is a user-level routine that you call in order to output memory info
// at a controlled time.
vis_update();
}
#scope_module
pending_stack_traces: [..] *Packed_Stack_Trace;
to_send_stack_traces: [..] *Packed_Stack_Trace;
// When we get a new stack trace, we pool the data items we need to send
// into these arrays, then bleed them off as we send messages.
to_send_strings: [..] string;
to_send_procedure_infos: [..] *Stack_Trace_Procedure_Info;
// The pending_stack_traces are only sent to us when we add a new one
// to a table from the core Memory_Debugger stuff. But there is no such
// tracking for strings or Procedure_Infos, so we do that ourselves,
// so we only need to send these once to the viewer.
table_sent_strings: Table(string, s32);
table_sent_procedure_infos: Table(*Stack_Trace_Procedure_Info, bool);
mutex_controlled_data_copy: [..] u8;
#scope_file
vis_channel: Write_Channel;
vis_enabled := false;
logged_retry := false;
vis_initted := false; // Did we try to call vis_init() yet?
did_user_level_per_frame_update := false;
last_update_time := APOLLO_TIME_INVALID;
last_vis_init_time := APOLLO_TIME_INVALID;
MILLISECONDS_PER_VIS_UPDATE :: 250;
MILLISECONDS_PER_VIS_INIT :: 500;
#import "Shared_Memory_Channel";
#import "Protocol_For_Memory_Visualization";
#import "Hash_Table";

590
modules/Basic/arena.jai Normal file
View File

@ -0,0 +1,590 @@
// #TODO: Consider how to approach ExpandableArenas
#load "arena_platform.jai";
// Counted in the arena's own pages, so this is 64 KB on a 4 KB-page arena and 32 MB on one
// backed by 2 MB large pages. Rounded up to a power of two by arena_init, which is what lets the
// arena store it as a log2.
ARENA_DEFAULT_COMMIT_PAGE_COUNT :: 16;
ARENA_DEFAULT_ALIGNMENT :: 8; // CPU register width
PLATFORM_MEMORY_PAGE_SIZE :: 4096;
PLATFORM_MEMORY_PAGE_SIZE_LOG2 :: 12;
#run assert((1 << PLATFORM_MEMORY_PAGE_SIZE_LOG2) == PLATFORM_MEMORY_PAGE_SIZE);
PLATFORM_CACHE_LINE_SIZE :: 64; // #Planned: unused today. Reserved for cache-line padding of
// per-thread or hot arena fields once those exist.
Arena_Sizes :: s64.[
64 * 1024,
2 * 1024 * 1024,
64 * 1024 * 1024,
2 * 1024 * 1024 * 1024,
64 * 1024 * 1024 * 1024,
2 * 1024 * 1024 * 1024 * 1024,
64 * 1024 * 1024 * 1024 * 1024,
];
Arena_Reserve :: enum u8 {
Size_64K :: 0;
Size_2M :: 1;
Size_64M :: 2;
Size_2G :: 3;
Size_64G :: 4;
Size_2T :: 5;
Size_64T :: 6;
}
#run assert(Arena_Sizes.count == type_info(Arena_Reserve).values.count);
Arena_Flags :: enum_flags u8 {
None :: 0;
Chained :: 0b00000001; // #Planned: chained/expandable arenas. NO_CHAIN is the only behaviour.
Is_Bootstrapped :: 0b00000010; // Live: set by arena_set_bootstrap_flag; read by arena_delete
// and free_pages_down_to.
Large_Pages :: 0b01000000; // Live: pass it to arena_init or bootstrap_arena. The reservation
// is backed by the platform's large/huge pages and the arena's
// page granularity becomes platform_large_page_size() rather
// than PLATFORM_MEMORY_PAGE_SIZE. Requesting it is a hint: if the
// OS refuses, the arena falls back to 4 KB pages and records the
// miss in Arena_Status.Large_Pages_Failed rather than failing to initialize.
//
// On Windows and macOS a granted request commits and wires the
// WHOLE reservation up front -- there is no reserve-now-commit-
// later form -- so the reserve class is what becomes resident.
// Those platforms also refuse partial decommit, which makes
// free_pages_down_to a no-op for such an arena.
//
// Large pages are already non-pageable, so a .Large_Pages arena
// that is also .Secure_Arena skips the per-commit page lock: the
// guarantee holds without it.
Secure_Arena :: 0b10000000; // Live: pass it to arena_init or bootstrap_arena. Every page the
// arena commits is locked into physical memory as it is committed,
// and unlocked before it is decommitted. Locking a range once at
// the top level does not survive this arena's growth or its
// release cycle, which is why it belongs on the commit path.
}
Arena_Status :: enum_flags u8 {
None :: 0;
Secure_Lock_Failed :: 0b00000001; // Sticky: a range that spent time unlocked cannot be made retroactively safe by a later success.
Large_Pages_Failed :: 0b00000010; // Sticky: pages already handed out at 4 KB granularity do not become large ones later.
}
arena_status_is_set :: (arena: *Arena, flag: Arena_Status) -> bool #expand {
return (arena.status & flag) == flag;
}
arena_is_secure :: (arena: *Arena) -> bool #expand {
return (arena.flags & .Secure_Arena) == .Secure_Arena;
}
// Whether a secure arena has actually held its guarantee.
arena_secure_lock_intact :: (arena: *Arena) -> bool {
if !is_valid(arena) return false;
return arena_is_secure(arena) && !arena_status_is_set(arena, .Secure_Lock_Failed);
}
arena_is_large_pages :: (arena: *Arena) -> bool #expand {
return (arena.flags & .Large_Pages) == .Large_Pages;
}
// Whether a large-page arena actually got large pages, as opposed to falling back to 4 KB.
arena_large_pages_active :: (arena: *Arena) -> bool {
if !is_valid(arena) return false;
return arena_is_large_pages(arena) && !arena_status_is_set(arena, .Large_Pages_Failed);
}
arena_page_size :: (arena: *Arena) -> s64 #expand {
return cast(s64) 1 << arena.page_size_log2;
}
arena_alignment :: (arena: *Arena) -> s64 #expand {
return cast(s64) 1 << arena.alignment_log2;
}
arena_initial_commit_pages :: (arena: *Arena) -> s64 #expand {
return cast(s64) 1 << arena.initial_commit_page_count_log2;
}
Arena :: struct {
current_point:*u8;
memory_base: *u8;
first_uncommitted_page: *u8;
reserve: Arena_Reserve;
flags: Arena_Flags;
status: Arena_Status;
alignment_log2: u8;
page_size_log2: u8 = PLATFORM_MEMORY_PAGE_SIZE_LOG2; // Set by arena_platform_init. The ground truth for every page calculation.
initial_commit_page_count_log2: u8;
#if ARENA_DEBUG {
high_water_mark: s64;
label: string;
file_path: string;
function_name: string;
line_number: s32;
}
}
bootstrap_arena :: (new_reserve: Arena_Reserve, default_commit_page_count: s32 = ARENA_DEFAULT_COMMIT_PAGE_COUNT, flags: Arena_Flags = .None, label:="", loc := #caller_location)
-> *Arena #expand {
arena_ptr := bootstrap_arena_internal(new_reserve, default_commit_page_count, flags, label, loc);
#if ARENA_DEBUG {
if arena_ptr {
push_allocator(context.default_allocator);
arena_ptr.label = copy_string(label);
arena_ptr.file_path = copy_string(loc.fully_pathed_filename);
this_allocation_is_not_a_leak(arena_ptr.label.data);
this_allocation_is_not_a_leak(arena_ptr.file_path.data);
arena_ptr.line_number = cast(s32)loc.line_number;
arena_ptr.function_name = #procedure_name();
}
}
return arena_ptr;
}
bootstrap_arena_internal :: (new_reserve: Arena_Reserve, default_commit_page_count: s32, flags: Arena_Flags, label: string, loc: Source_Code_Location) -> *Arena {
new_arena: Arena;
success, pages_committed := arena_init(*new_arena, new_reserve, default_commit_page_count, flags);
if !success || !pages_committed {
arena_delete(*new_arena);
return null;
}
arena_ptr: *Arena = xx new_arena.memory_base;
memcpy(arena_ptr, *new_arena, size_of(Arena));
arena_ptr.current_point = arena_start(arena_ptr);
arena_set_bootstrap_flag(arena_ptr);
return arena_ptr;
}
arena_allocator_proc :: (mode: Allocator_Mode, requested_size: s64, old_size: s64, old_memory: *void, allocator_data: *void) -> *void {
arena := cast(*Arena)allocator_data;
if #complete mode == {
case .FREE;
return null;
case .RESIZE; #through;
case .ALLOCATE;
result := arena_alloc(arena, requested_size);
if mode == .RESIZE {
size_to_copy := ifx old_size < requested_size then old_size else requested_size;
if result && size_to_copy memcpy(result, old_memory, size_to_copy);
}
return result;
case .STARTUP; #through;
case .SHUTDOWN;
return null;
case .THREAD_START; #through;
case .THREAD_STOP;
return null;
case .CREATE_HEAP; #through;
case .DESTROY_HEAP;
return null;
case .IS_THIS_YOURS;
if !arena return null;
// The contract is 'allocated by your heap and is currently live'.
if old_memory >= arena_start(arena) && old_memory < arena.current_point {
return cast(*void)true;
}
return null;
case .CAPS;
if old_memory { (cast(*string)old_memory).* = "modules/Basic arena_allocator_proc"; }
return cast(*void)(Allocator_Caps.IS_THIS_YOURS | .HINT_I_AM_A_FAST_BUMP_ALLOCATOR);
}
}
arena_init :: (using arena: *Arena, new_reserve: Arena_Reserve, commit_page_count:=ARENA_DEFAULT_COMMIT_PAGE_COUNT, new_flags: Arena_Flags = .None)
-> (success: bool, pages_committed: s64 = 0) {
if arena == null {
log_error("[Basic/Arena] Attempting to initialize a null arena!");
return false;
}
if (arena.memory_base == null) {
// Before arena_platform_init, so .Large_Pages reaches the reserve call, and before the first
// commit, so .Secure_Arena locks through the same path every later growth uses.
flags |= new_flags;
if !arena_platform_init(arena, new_reserve) {
return false;
}
current_point = arena_start(arena);
arena_set_alignment(arena, ARENA_DEFAULT_ALIGNMENT);
commit_pages := ifx commit_page_count > 0
then cast(s64) 1 << ceil_log2(commit_page_count)
else cast(s64) commit_page_count;
// Page size is not known until the reserve has happened, so the commit size is sized here.
commit_size := commit_pages * arena_page_size(arena);
if commit_size < 0 {
log_warning("[Basic/Arena] Invalid commit_page_count passed to `arena_init`. Using default parameter: % pages",
ARENA_DEFAULT_COMMIT_PAGE_COUNT);
commit_size = ARENA_DEFAULT_COMMIT_PAGE_COUNT * arena_page_size(arena);
}
bytes_committed := arena_commit_first_pages(arena, commit_size);
committed_pages := bytes_committed >> page_size_log2;
if committed_pages <= 0 return true, 0; // The commit failed; bootstrap_arena_internal checks for this.
initial_commit_page_count_log2 = log2_of_power_of_two(committed_pages);
return true, committed_pages;
}
return true, arena_initial_commit_pages(arena); // already initialized
}
// start_offset_bytes is for when we're doing arena-backed structures such as Arena_Array and we may need more
// space than `commit_size` because we're using some space in the first page.
// #Planned: no caller passes it yet -- Arena_Array does not exist. It is implemented and guarded,
// so it works if you pass it, but it is untested and should be treated as such until it has a user.
arena_commit_first_pages :: (using arena: *Arena, commit_size: s64, start_offset_bytes: s64 = 0)
-> (bytes_committed: s64) {
assert(start_offset_bytes >= 0);
if (start_offset_bytes < 0) {
log_error("[Basic/Arena] Invalid start_offset_bytes parameter provided to arena_commit_first_pages! Given: %", start_offset_bytes);
return 0;
}
commit_size_final := commit_size;
// Clamp commit size:
if commit_size <= 0 {
commit_size_final = ARENA_DEFAULT_COMMIT_PAGE_COUNT * arena_page_size(arena);
log_warning("[Basic/Arena] Requested commit size is invalid! Got % expected 0 < commit_size <= %. Using default of %!",
commit_size, reserve_size_bytes(arena), commit_size_final);
}
if commit_size > reserve_size_bytes(arena) {
log_warning("[Basic/Arena] Requested commit size is out of expected range! Got % expected 0 < commit_size <= %. Clamping to max size!",
commit_size, reserve_size_bytes(arena));
commit_size_final = reserve_size_bytes(arena);
}
final_address := Align(memory_base + commit_size_final + start_offset_bytes, arena_page_size(arena));
commit_size_final = cast(s64)(final_address - memory_base);
if (commit_size_final > reserve_size_bytes(arena)) {
log_warning("[Basic/Arena] Requested commit size larger than available address space! Got % expected <= %. Clamping to max size!",
commit_size_final, reserve_size_bytes(arena));
commit_size_final = reserve_size_bytes(arena);
}
final_address = memory_base + commit_size_final;
if !extend_committed_pages(arena, final_address) {
return 0;
}
return arena_committed_bytes(arena);
}
arena_reset_keeping_memory :: arena_reset;
arena_reset :: (using arena: *Arena) {
if !is_valid(arena) return;
current_point = arena_start(arena);
}
arena_release :: (using arena: *Arena) {
if !is_valid(arena) return;
current_point = arena_start(arena);
free_pages_down_to(arena, arena_initial_commit_pages(arena));
}
arena_reset_overwriting_memory :: (using arena: *Arena, $wipe_function:=memory_secure_overwrite) {
if !is_valid(arena) return;
arena_wipe(arena_start(arena), current_point, wipe_function);
arena_reset(arena);
}
arena_alloc :: (using arena: *Arena, byte_count: s64) -> *void {
if !arena {
log_error("[Basic/Arena] Passed a null pointer to arena_alloc!");
return null;
}
if arena.memory_base == null {
log_error("Attempting to allocate using an uninitialized Arena!\n Initializing with a 64GB capacity.");
arena.reserve = .Size_64G;
success := arena_init(arena, arena.reserve);
if !success return null;
}
if byte_count < 0 {
log_error("[Basic/Arena] Arena %: negative allocation size %.\n", arena, byte_count);
return null;
}
result := Align(current_point, arena_alignment(arena));
if byte_count > arena_address_limit(arena) - result {
log_error("[Basic/Arena] Attempt to allocate, but Arena % is full! (Allocation size %; alignment %; address space size %; already allocated %.)\n",
arena, byte_count, arena_alignment(arena), reserve_size_bytes(arena), current_point - memory_base);
return null;
}
result_end := byte_count + result;
if result_end > first_uncommitted_page {
if !extend_committed_pages(arena, result_end) return null;
}
current_point = result_end;
update_high_water_mark(arena);
return result;
}
arena_secure_overwrite_starting_from :: (arena: *Arena, start: *u8) {
arena_wipe(start, arena.first_uncommitted_page);
}
Arena_Secure_Overwrite_Endpoint :: enum s32 {
Current_Point; // erase from arena_start to the current point
First_Uncommitted_Page;
}
arena_secure_overwrite :: (arena: *Arena, $endpoint: Arena_Secure_Overwrite_Endpoint=.Current_Point) {
#if endpoint == {
case .Current_Point;
arena_wipe(arena_start(arena), arena.current_point);
case .First_Uncommitted_Page;
arena_wipe(arena_start(arena), arena.first_uncommitted_page);
}
}
// Arena @Utility
arena_set_alignment :: inline (arena: *Arena, alignment: s64) {
assert(is_valid(arena));
assert(alignment > 0 && (alignment & (alignment - 1)) == 0,
"Arena alignment must be a non-zero power of two; got %.", alignment);
if alignment <= 0 || (alignment & (alignment - 1)) != 0 {
log_error("[Basic/Arena][arena_set_alignment] Attempting to set an invalid alignment `%`! Keeping original value `%`",
alignment, arena_alignment(arena));
return;
}
limit := reserve_size_bytes(arena);
if alignment > limit {
log_error("[Basic/Arena][arena_set_alignment] Attempting to set an alignment `%` wider than the % byte reservation! Clamping to the reservation",
alignment, limit);
arena.alignment_log2 = log2_of_power_of_two(limit);
return;
}
arena.alignment_log2 = log2_of_power_of_two(alignment);
}
arena_set_bootstrap_flag :: (arena: *Arena) #expand {
arena.flags = (arena.flags | .Is_Bootstrapped);
}
arena_is_bootstrapped :: (arena: *Arena) -> bool #expand {
return (arena.flags & .Is_Bootstrapped == .Is_Bootstrapped);
}
update_high_water_mark :: (arena: *Arena) #expand {
#if ARENA_DEBUG {
current_usage := arena_usage_bytes(arena);
if (current_usage > arena.high_water_mark) {
arena.high_water_mark = current_usage;
}
}
}
// Every arena reserves `size_of(Arena)` bytes at its base, whether or not it is bootstrapped.
//
// The reason is not the cost of testing `arena_is_bootstrapped` here. This is a macro, and
// `flags` shares a cache line with `memory_base`, so the test would be a byte load, an AND and a
// CMOV, predicted perfectly because the bit never changes after init, less than `arena_alloc`
// already spends around this call. What a branch would cost instead:
//
// - `arena_start` would stop being a pure function of `memory_base`, which is written once at
// init. Every caller that compares two of its results across an arena's life: `arena_usage_bytes`,
// the IS_THIS_YOURS range test, the reset paths, the `Arena_State` marks
// depends on it not moving, and `flags` is mutable and public.
//
// - The flag is not set yet at the two places that need it most. `arena_init` runs on a stack
// local before anything knows the arena will be bootstrapped, and `bootstrap_arena_internal`
// computes `current_point` from here one line before `arena_set_bootstrap_flag`, so a
// branching version would park `current_point` on top of the header it means to skip.
//
// Note that `size_of(Arena)` is larger under ARENA_DEBUG, so `arena_start` does not return the
// same offset in debug and release builds. *Never persist an offset into arena memory across a
// change of build configuration*
arena_start :: (arena: *Arena) -> *u8 #expand {
return Align(arena.memory_base + size_of(Arena), ARENA_DEFAULT_ALIGNMENT);
}
arena_address_limit :: (arena: *Arena) -> *u8 #expand {
return (arena.memory_base + reserve_size_bytes(arena));
}
arena_usage_bytes :: (arena: *Arena) -> s64 #expand {
return cast(s64)(arena.current_point - arena_start(arena));
}
arena_committed_bytes :: (arena: *Arena) -> s64 #expand {
return cast(s64)(arena.first_uncommitted_page - arena.memory_base);
}
reserve_size_bytes :: (arena_reserve: Arena_Reserve) -> s64 {
return Arena_Sizes[cast(s32)arena_reserve];
}
reserve_size_bytes :: (arena: *Arena) -> s64 #expand {
return reserve_size_bytes(arena.reserve);
}
is_valid :: (arena: *Arena) -> bool {
return (arena != null && arena.memory_base != null);
}
get_allocator :: (arena: *Arena) -> Allocator #expand {
return .{arena_allocator_proc, arena};
}
next_reserve :: (size: s64, alignment: s64) -> Arena_Reserve {
size_with_overhead := Align(size + size_of(Arena), alignment);
for i: 0..enum_count(Arena_Reserve)-1 {
if size_with_overhead <= Arena_Sizes[i] {
return cast(Arena_Reserve)i;
}
}
return .Size_64T;
}
// @Utility `push_allocator` style macros
push_arena :: (arena: *Arena) #expand #no_debug {
assert(is_valid(arena));
old_allocator := context.allocator;
context.allocator = .{arena_allocator_proc, arena};
`defer context.allocator = old_allocator;
}
push_alignment :: (arena: *Arena, alignment: s64) #expand #no_debug {
assert(is_valid(arena));
assert(alignment > 0 && (alignment & (alignment - 1)) == 0,
"Arena alignment must be a non-zero power of two; got %.", alignment);
old_alignment := arena_alignment(arena);
if alignment <= 0 || (alignment & (alignment - 1)) != 0 {
log_error("[Basic/Arena][push_alignment] Attempting to set an invalid alignment `%`! Keeping original value `%`",
alignment, old_alignment);
return;
}
arena_set_alignment(arena, alignment);
`defer arena_set_alignment(arena, old_alignment);
}
auto_reset_overwriting_memory :: (arena: *Arena, $wipe_function:=memory_secure_overwrite) #expand #no_debug {
__mark := get_arena_state(arena);
`defer restore_arena_state(arena, __mark, false, wipe_function);
}
auto_release_overwriting_memory :: (arena: *Arena, $wipe_function:=memory_secure_overwrite) #expand #no_debug {
__mark := get_arena_state(arena);
`defer restore_arena_state(arena, __mark, true, wipe_function);
}
auto_reset :: (arena: *Arena) #expand {
__mark := get_arena_state(arena);
`defer restore_arena_state(arena, __mark, false, wipe_function=null);
}
auto_release :: (arena: *Arena) #expand {
__mark := get_arena_state(arena);
`defer restore_arena_state(arena, __mark, true, wipe_function=null);
}
// Temporary for debugging:
arena_details :: (arena: *Arena, loc := #caller_location) -> string {
push_allocator(temp);
builder: String_Builder;
if arena == null {
print_to_builder(*builder, "(arena_details) Passed a null pointer @ %\n", loc);
return builder_to_string(*builder);
}
print_to_builder(*builder, "Details for Arena % with memory_base %\n", arena, arena.memory_base);
if arena.memory_base == null {
return builder_to_string(*builder);
}
reserve := arena_address_limit(arena) - arena.memory_base;
committed_size := arena.first_uncommitted_page - arena.memory_base;
used_size := arena.current_point - arena_start(arena);
print_to_builder(*builder, " - committed: %, used: %, reserve: %\n", format_bytes(committed_size), format_bytes(used_size), format_bytes(reserve));
print_to_builder(*builder, " - page size: %\n", format_bytes(arena_page_size(arena)));
if arena_is_large_pages(arena) {
print_to_builder(*builder, " - large pages: % (requested)\n",
ifx arena_status_is_set(arena, .Large_Pages_Failed) then "unavailable, fell back" else "active");
}
return builder_to_string(*builder);
}
arena_wipe :: (start: *u8, end: *u8, $wipe_function:=memory_secure_overwrite) #expand {
#if wipe_function {
if end <= start return;
wipe_function(start, cast(s64)(end - start));
}
}
#scope_module
Arena_State :: struct {
starting_arena : *Arena;
starting_point : *u8;
}
get_arena_state :: inline (arena: *Arena) -> Arena_State {
assert(arena != null);
state: Arena_State = ---;
state.starting_arena = arena;
state.starting_point = state.starting_arena.current_point;
return state;
}
restore_arena_state :: inline (arena: *Arena, state: Arena_State, $free_pages: bool, $wipe_function:=memory_secure_overwrite) {
assert(arena == state.starting_arena);
assert(is_valid(state.starting_arena));
#if wipe_function {
arena_wipe(state.starting_point, state.starting_arena.current_point, wipe_function);
}
state.starting_arena.current_point = state.starting_point;
#if free_pages {
using state.starting_arena;
last_page := cast(*void)Align(current_point, arena_page_size(arena));
keep_page_count := cast(s64)(last_page - memory_base) >> page_size_log2;
initial_pages := arena_initial_commit_pages(arena);
if keep_page_count < initial_pages keep_page_count = initial_pages;
free_pages_down_to(arena, keep_page_count);
}
}

View File

@ -0,0 +1,494 @@
arena_delete :: (arena: *Arena) {
if (!is_valid(arena)) return;
#if ARENA_DEBUG {
free(arena.label,, context.default_allocator);
free(arena.file_path,, context.default_allocator);
}
bootstrapped := arena_is_bootstrapped(arena);
#if OS == .WINDOWS {
success := VirtualFree(arena.memory_base, 0, MEM_RELEASE) != .FALSE;
if !success {
value, description := System.get_error_value_and_string();
log_error("[Basic/Arena] In arena_delete, VirtualFree failed with code %: %\n", value, description);
return;
}
}
else #if OS_IS_UNIX {
success := P.munmap(arena.memory_base, cast(u64)reserve_size_bytes(arena)) == 0;
if !success {
value, description := System.get_error_value_and_string();
log_error("[Basic/Arena] In arena_delete, munmap failed with code %: %\n", value, description);
return;
}
}
if !bootstrapped arena.memory_base = null;
}
// Windows MEM_LARGE_PAGES and Darwin superpages both commit and wire the whole reservation at
// map time, and both refuse a partial decommit of it. Linux hugetlb pages can still be dropped
// with madvise, so only these two make free_pages_down_to a no-op.
LARGE_PAGES_ARE_FULLY_COMMITTED :: OS == .WINDOWS || OS == .MACOS;
#scope_module
UNQUERIED :: -1; // Sentinel for the cached large page size, which is legitimately 0 when unsupported.
// Whether this arena locks and unlocks its own pages as it commits and decommits them.
//
// Large pages are non-pageable by construction, so a .Secure_Arena that got them already holds
// the residency guarantee lock_range exists to provide. Locking them again would at best be a
// wasted syscall on the allocation path and at worst a spurious .Secure_Lock_Failed -- and the
// unlock paths have to agree, or they would unlock a range that was never locked.
arena_manages_page_locks :: (arena: *Arena) -> bool #expand {
return arena_is_secure(arena) && !arena_large_pages_active(arena);
}
// Whether this arena should try for large pages, and at what granularity. Only what the OS call
// itself requires is checked: there is deliberately no ceiling on the reserve class, so a caller
// asking for large pages on a huge reservation gets what it asked for on the platforms that
// commit the whole thing.
large_page_granularity_for :: (arena: *Arena, size: s64) -> (page_size: s64, eligible: bool) {
if !arena_is_large_pages(arena) return 0, false;
large := platform_large_page_size();
if large <= 0 || (large & (large - 1)) != 0 {
// Every real large page size is a power of two; refusing a reported non-power-of-two keeps
// the log2 that all the page arithmetic runs on well defined.
log_warning("[Basic/Arena] .Large_Pages was requested but this machine reports no usable large page size (got %). Using % byte pages.",
large, PLATFORM_MEMORY_PAGE_SIZE);
return 0, false;
}
if size < large || (size % large) != 0 {
log_warning("[Basic/Arena] .Large_Pages was requested for a % byte reservation, which is not a whole multiple of the % byte large page size. Using % byte pages.",
size, large, PLATFORM_MEMORY_PAGE_SIZE);
return 0, false;
}
return large, true;
}
log2_of_power_of_two :: (value: s64) -> u8 {
assert(value > 0 && (value & (value - 1)) == 0);
result: u8 = 0;
remaining := value;
while remaining != 1 {
remaining >>= 1;
result += 1;
}
return result;
}
ceil_log2 :: (value: s64) -> u8 {
assert(value >= 1);
result: u8 = 0;
while (cast(s64) 1 << result) < value result += 1;
return result;
}
#scope_export
#if OS == .WINDOWS {
platform_large_page_size :: () -> s64 {
// Reports what the CPU supports, independently of whether the process holds
// SeLockMemoryPrivilege. Without the privilege the VirtualAlloc below simply fails, which
// the fallback already handles, and granting it is the main program's job.
if cached_large_page_size == UNQUERIED cached_large_page_size = cast(s64) GetLargePageMinimum();
return cached_large_page_size;
}
arena_platform_init :: (using arena: *Arena, new_reserve: Arena_Reserve) -> bool {
reserve = new_reserve;
size := reserve_size_bytes(new_reserve);
page_size_log2 = PLATFORM_MEMORY_PAGE_SIZE_LOG2;
large_page_size, eligible := large_page_granularity_for(arena, size);
if eligible {
// MEM_LARGE_PAGES has no reserve-only form, so this one call commits and wires all `size`
// bytes. That is why first_uncommitted_page starts at the top: there is nothing left for
// extend_committed_pages to commit, and MEM_DECOMMIT cannot take part of it back.
p := VirtualAlloc(null, cast(u64) size, MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES, PAGE_READWRITE);
if p {
memory_base = p;
page_size_log2 = log2_of_power_of_two(large_page_size);
first_uncommitted_page = memory_base + size;
return true;
}
// Usually a missing SeLockMemoryPrivilege, or no free physical run of `size` contiguous
// bytes. Neither is fatal: the 4 KB reservation below is a working arena.
value, description := System.get_error_value_and_string();
log_warning("[Basic/Arena] Large page VirtualAlloc of % bytes failed with code %: %. Falling back to % byte pages.",
size, value, description, PLATFORM_MEMORY_PAGE_SIZE);
}
if arena_is_large_pages(arena) status |= .Large_Pages_Failed;
memory_base = VirtualAlloc(null, cast(u64) size, MEM_RESERVE, PAGE_READWRITE);
success := memory_base != null;
if !success {
value, description := System.get_error_value_and_string();
log_error("[Basic/Arena] In arena_platform_init, VirtualAlloc failed with code %: %\n", value, description);
return false;
}
first_uncommitted_page = memory_base;
return true;
}
// A fully committed large-page arena never reaches the commit call below: its
// first_uncommitted_page already sits at the top of the reservation, so `delta` cannot be
// positive and this returns at the guard.
extend_committed_pages :: (arena: *Arena, end: *u8) -> bool {
delta := cast(s64)(end - arena.first_uncommitted_page);
if delta <= 0 return true;
size := Align(delta, arena_page_size(arena));
p := VirtualAlloc(arena.first_uncommitted_page, cast(u64)size, MEM_COMMIT, PAGE_READWRITE);
if !p {
value, description := System.get_error_value_and_string();
log_error("[Basic/Arena] In extend_committed_pages, VirtualAlloc failed with code %: %\n", value, description);
return false;
}
if arena_manages_page_locks(arena) {
if !lock_range(arena.first_uncommitted_page, size) arena.status |= .Secure_Lock_Failed;
}
arena.first_uncommitted_page += size;
return true;
}
#scope_module
cached_large_page_size: s64 = UNQUERIED;
lock_range :: (start: *u8, byte_count: s64) -> bool {
if byte_count <= 0 return true;
if cast(bool) VirtualLock(start, cast(u64)byte_count) return true;
value, description := System.get_error_value_and_string();
log_error("[Basic/Arena] VirtualLock of % bytes failed with code %: %\n", byte_count, value, description);
return false;
}
unlock_range :: (start: *u8, byte_count: s64) -> bool {
if byte_count <= 0 return true;
if cast(bool) VirtualUnlock(start, cast(u64)byte_count) return true;
value, description := System.get_error_value_and_string();
log_error("[Basic/Arena] VirtualUnlock of % bytes failed with code %: %\n", byte_count, value, description);
return false;
}
free_pages_down_to :: (arena: *Arena, pages_to_keep: s64) {
if (arena == null) return;
if arena_is_bootstrapped(arena) assert(pages_to_keep >= 1, "Always keep at least 1 page if we're bootstrapping arena");
// A large-page allocation is released as a whole or not at all -- there is no partial
// MEM_DECOMMIT for one -- so such an arena rewinds but never drops below its reservation.
#if LARGE_PAGES_ARE_FULLY_COMMITTED {
if arena_large_pages_active(arena) return;
}
bytes_to_keep := pages_to_keep * arena_page_size(arena);
assert(bytes_to_keep <= reserve_size_bytes(arena), "Requesting to keep more bytes than there is address space!");
bytes_committed := cast(s64)(arena.first_uncommitted_page - arena.memory_base);
if (bytes_to_keep > bytes_committed) {
return; // nothing to do
}
start_address := arena.memory_base + bytes_to_keep;
bytes_to_decommit := cast(u64)(arena.first_uncommitted_page - start_address);
// `VirtualFree(start_address, 0, MEM_DECOMMIT)` decommits the entire region:
if bytes_to_decommit {
// Decommitting drops the lock on these pages anyway; unlocking first keeps the process's
// locked-page accounting honest, so the next commit is not refused against a stale total.
if arena_manages_page_locks(arena) unlock_range(start_address, cast(s64)bytes_to_decommit);
VirtualFree(start_address, bytes_to_decommit, MEM_DECOMMIT);
arena.first_uncommitted_page = start_address;
}
}
#scope_export
arena_lock_pages :: (arena: *Arena) -> bool {
if !is_valid(arena) return false;
// Large pages are already non-pageable, so the guarantee this provides is one the arena
// holds either way. Reporting success without the syscall keeps it paired with the unlock.
if arena_large_pages_active(arena) return true;
byte_count := arena_committed_bytes(arena);
if byte_count == 0 {
log_error("Attempting to lock pages in an arena with no active pages!");
return false;
}
return lock_range(arena.memory_base, byte_count);
}
arena_unlock_pages :: (arena: *Arena) -> bool {
if !is_valid(arena) return false;
if arena_large_pages_active(arena) return true; // Nothing was locked; see arena_lock_pages.
byte_count := arena_committed_bytes(arena);
if byte_count == 0 return true;
return unlock_range(arena.memory_base, byte_count);
}
} else #if OS_IS_UNIX {
P :: #import "POSIX"; // Can remove this dependency since we only use a few things.
platform_large_page_size :: () -> s64 {
if cached_large_page_size != UNQUERIED return cached_large_page_size;
#if OS == .MACOS {
// Darwin superpages are an x86_64-only facility; Apple Silicon exposes no user-facing
// equivalent, so there is nothing to report there.
#if CPU == .X64 {
cached_large_page_size = 2 * 1024 * 1024;
} else {
cached_large_page_size = 0;
}
} else {
cached_large_page_size = linux_default_huge_page_size();
}
return cached_large_page_size;
}
arena_platform_init :: (using arena: *Arena, new_reserve: Arena_Reserve) -> bool {
reserve = new_reserve;
size := reserve_size_bytes(new_reserve);
page_size_log2 = PLATFORM_MEMORY_PAGE_SIZE_LOG2;
PROT :: P.PROT_READ | P.PROT_WRITE;
FLAGS :: P.MAP_PRIVATE | P.MAP_ANONYMOUS;
large_page_size, eligible := large_page_granularity_for(arena, size);
if eligible {
mapped: *void = P.MAP_FAILED;
#if OS == .MACOS {
// There is no mmap flag for superpages; the size class travels in the fd argument.
// Wired and fully committed at map time, the way Windows large pages are.
mapped = P.mmap(null, cast(u64) size, PROT, FLAGS, VM_FLAGS_SUPERPAGE_SIZE_2MB, 0);
} else {
// MAP_HUGETLB draws from the hugetlbfs pool an administrator preallocated with
// vm.nr_hugepages, and returns a huge-page-aligned address. It is the only form that
// actually guarantees the granularity, so it is what gets tried.
mapped = P.mmap(null, cast(u64) size, PROT, FLAGS | P.MAP_HUGETLB, 0, 0);
}
if mapped != P.MAP_FAILED {
memory_base = mapped;
page_size_log2 = log2_of_power_of_two(large_page_size);
#if LARGE_PAGES_ARE_FULLY_COMMITTED {
first_uncommitted_page = memory_base + size; // Wired in full at map time; nothing left to commit.
} else {
first_uncommitted_page = memory_base;
}
return true;
}
// On Linux this is the common case: no preallocated hugetlbfs pool.
value, text := System.get_error_value_and_string();
log_warning("[Basic/Arena] Large page mmap of % bytes failed with code %: %. Falling back to % byte pages.",
size, value, text, PLATFORM_MEMORY_PAGE_SIZE);
}
if arena_is_large_pages(arena) status |= .Large_Pages_Failed;
memory_base = P.mmap(null, cast(u64) size, PROT, FLAGS, 0, 0);
success := memory_base != P.MAP_FAILED;
if !success {
value, text := System.get_error_value_and_string();
log_error("[Basic/Arena] In Arena, mmap failed with code %: %\n", value, text);
memory_base = null;
return false;
}
#if OS == .LINUX {
// Transparent huge pages: strictly advisory. The kernel may back the range with huge pages
// and may split them again, so the arena keeps 4 KB arithmetic and still reports
// arena_large_pages_active as false. It costs one syscall and may recover most of the TLB
// win the hugetlbfs pool would have given, so a caller that asked gets it anyway.
if arena_is_large_pages(arena) P.madvise(memory_base, cast(u64) size, P.MADV_HUGEPAGE);
}
first_uncommitted_page = memory_base;
return true;
}
extend_committed_pages :: (arena: *Arena, end: *u8) -> bool {
delta := end - arena.first_uncommitted_page;
if delta <= 0 return true;
size := Align(delta, arena_page_size(arena));
// mlock forces the pages resident immediately rather than on first touch.
//
// Recorded rather than propagated, for the reason given in the Windows arm: a low
// RLIMIT_MEMLOCK must not become an out-of-memory.
if arena_manages_page_locks(arena) {
if !lock_range(arena.first_uncommitted_page, size) arena.status |= .Secure_Lock_Failed;
}
arena.first_uncommitted_page += size;
return true;
}
#scope_module
cached_large_page_size: s64 = UNQUERIED;
#if OS != .MACOS {
// `Hugepagesize:` in /proc/meminfo is the size MAP_HUGETLB maps at when no explicit size is
// requested. Basic cannot import File -- File imports Basic -- so this goes through libc
// directly, in the style of the nested bindings in posix.jai.
linux_default_huge_page_size :: () -> s64 {
FALLBACK :: 2 * 1024 * 1024; // What x86_64 and 4 KB-granule arm64 both default to.
KEY :: "Hugepagesize:";
fd := P.open("/proc/meminfo".data, P.O_RDONLY);
if fd < 0 return FALLBACK;
defer P.close(fd);
// /proc/meminfo reports a size of 0, so it has to be read rather than stat'd. The key sits
// in the first few hundred bytes in practice; one page is a generous margin.
buffer: [4096] u8 = ---;
bytes_read := P.read(fd, buffer.data, cast(u64) buffer.count);
if bytes_read <= 0 return FALLBACK;
at := 0;
while at + KEY.count <= bytes_read {
matched := true;
for i: 0 .. KEY.count - 1 {
if buffer[at + i] != KEY[i] {
matched = false;
break;
}
}
if matched break;
at += 1;
}
if at + KEY.count > bytes_read return FALLBACK;
cursor := at + KEY.count;
while cursor < bytes_read && buffer[cursor] == #char " " cursor += 1;
kilobytes: s64 = 0; // The file states its own unit, and it is always kB for this key.
while cursor < bytes_read && buffer[cursor] >= #char "0" && buffer[cursor] <= #char "9" {
kilobytes = kilobytes * 10 + cast(s64)(buffer[cursor] - #char "0");
cursor += 1;
}
if kilobytes <= 0 return FALLBACK;
return kilobytes * 1024;
}
}
lock_range :: (start: *u8, byte_count: s64) -> bool {
if byte_count <= 0 return true;
if P.mlock(start, cast(u64)byte_count) == 0 return true;
value, description := System.get_error_value_and_string();
log_error("[Basic/Arena] mlock of % bytes failed with code %: %\n", byte_count, value, description);
return false;
}
unlock_range :: (start: *u8, byte_count: s64) -> bool {
if byte_count <= 0 return true;
if P.munlock(start, cast(u64)byte_count) == 0 return true;
value, description := System.get_error_value_and_string();
log_error("[Basic/Arena] munlock of % bytes failed with code %: %\n", byte_count, value, description);
return false;
}
free_pages_down_to :: (using arena: *Arena, pages_to_keep: s64) {
if (arena == null) return;
if arena_is_bootstrapped(arena) assert(pages_to_keep >= 1, "Always keep at least 1 page if we're bootstrapping arena");
// Darwin superpages are wired at map time and cannot be partially released; Linux hugetlb
// pages can, at huge-page granularity, which page_size_log2 already accounts for.
#if LARGE_PAGES_ARE_FULLY_COMMITTED {
if arena_large_pages_active(arena) return;
}
bytes_to_keep := pages_to_keep * arena_page_size(arena);
assert(bytes_to_keep <= reserve_size_bytes(arena), "Requesting to keep more bytes than there is address space!");
bytes_committed := cast(s64)(arena.first_uncommitted_page - arena.memory_base);
if (bytes_to_keep > bytes_committed) {
return; // nothing to do
}
start_address := arena.memory_base + bytes_to_keep;
bytes_to_decommit := cast(u64)(arena.first_uncommitted_page - start_address);
if bytes_to_decommit {
if arena_manages_page_locks(arena) unlock_range(start_address, cast(s64)bytes_to_decommit);
if P.madvise(start_address, bytes_to_decommit, P.MADV_DONTNEED) != 0 {
value, description := System.get_error_value_and_string();
log_error("[Basic/Arena] In free_pages_down_to, madvise failed with code %: %\n", value, description);
}
arena.first_uncommitted_page = start_address;
}
}
#scope_export
arena_lock_pages :: (arena: *Arena) -> bool {
if !is_valid(arena) return false;
// Large pages are already non-pageable, so the guarantee this provides is one the arena
// holds either way. Reporting success without the syscall keeps it paired with the unlock.
if arena_large_pages_active(arena) return true;
byte_count := arena_committed_bytes(arena);
if byte_count == 0 {
log_error("Attempting to lock pages in an arena with no active pages!");
return false;
}
return lock_range(arena.memory_base, byte_count);
}
arena_unlock_pages :: (arena: *Arena) -> bool {
if !is_valid(arena) return false;
if arena_large_pages_active(arena) return true; // Nothing was locked; see arena_lock_pages.
byte_count := arena_committed_bytes(arena);
if byte_count == 0 return true;
return unlock_range(arena.memory_base, byte_count);
}
}
Align :: inline (n: $T, $$ alignment: s64) -> T {
assert(alignment > 0 && (alignment & (alignment - 1)) == 0);
return (n + alignment-1) & ~(alignment-1);
}
System :: #import "System";
#scope_file
#if OS == .WINDOWS {
kernel32 :: #library,system "kernel32";
VirtualAlloc :: (lpAddress: *void, dwSize: u64, flAllocationType: u32, flProtect: u32) -> *void #foreign kernel32;
VirtualFree :: (lpAddress: *void, dwSize: u64, dwFreeType: u32) -> BOOL #foreign kernel32;
VirtualLock :: (lpAddress: *void, dwSize: u64) -> BOOL #foreign kernel32;
VirtualUnlock :: (lpAddress: *void, dwSize: u64) -> BOOL #foreign kernel32;
GetLargePageMinimum :: () -> u64 #foreign kernel32;
PAGE_READWRITE :: 0x04;
MEM_COMMIT :: 0x00001000;
MEM_RESERVE :: 0x00002000;
MEM_DECOMMIT :: 0x00004000;
MEM_RELEASE :: 0x00008000;
MEM_LARGE_PAGES :: 0x20000000;
BOOL :: enum s32 { FALSE :: 0; TRUE :: 1; }
}
else #if OS == .MACOS {
// From macOS/bindings/mach.jai. mmap takes these in its fd argument rather than its flags.
SUPERPAGE_SIZE_2MB :: 2;
VM_FLAGS_SUPERPAGE_SHIFT :: 16;
VM_FLAGS_SUPERPAGE_SIZE_2MB :: SUPERPAGE_SIZE_2MB << VM_FLAGS_SUPERPAGE_SHIFT;
}

View File

@ -0,0 +1,68 @@
Copyright for the MAPM code used in string_to_float.jai:
MAPM
Version 4.9.5a
February 21, 2010
Michael C. Ring
ringx004@umn.edu
Latest release will be available at
http://tc.umn.edu/~ringx004
***************************************************************************
* *
* Copyright (C) 1999 - 2010 Michael C. Ring *
* *
* This software is Freeware. *
* *
* Permission to use, copy, and distribute this software and its *
* documentation for any purpose with or without fee is hereby granted, *
* provided that the above copyright notice appear in all copies and *
* that both that copyright notice and this permission notice appear *
* in supporting documentation. *
* *
* Permission to modify the software is granted. Permission to distribute *
* the modified code is granted. Modifications are to be distributed by *
* using the file 'license.txt' as a template to modify the file header. *
* 'license.txt' is available in the official MAPM distribution. *
* *
* To distribute modified source code, insert the file 'license.txt' *
* at the top of all modified source code files and edit accordingly. *
* *
* This software is provided "as is" without express or implied warranty. *
* *
***************************************************************************
See the actual MAPM distribution for full information.
Copyright for the string_to_float64 code in string_to_float.jai:
MIT License
Copyright (c) 2018 Albert Chan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

839
modules/Basic/docs/Arena.md Normal file
View File

@ -0,0 +1,839 @@
# Arena
A reserve-and-commit bump allocator. `arena.jai` holds the portable logic, `arena_platform.jai`
the Windows and POSIX virtual-memory calls.
- [Model](#model)
- [Quick start](#quick-start)
- [Constants and types](#constants-and-types)
- [Lifecycle](#lifecycle)
- [Allocation](#allocation)
- [Alignment](#alignment)
- [Resetting and releasing](#resetting-and-releasing)
- [Scoped helpers](#scoped-helpers)
- [Allocator integration](#allocator-integration)
- [Secure erase and page locking](#secure-erase-and-page-locking)
- [Introspection](#introspection)
- [Platform layer](#platform-layer)
- [Thread safety](#thread-safety)
- [Correctness notes and limitations](#correctness-notes-and-limitations)
- [Planned features](#planned-features)
- [Testing](#testing)
---
## Model
An arena takes one large reservation of address space up front, commits physical pages behind
it on demand, and hands out memory by advancing a pointer. Individual allocations are never
freed; you reclaim in bulk by rewinding to the start or to a saved mark.
Four boundaries describe an arena, three of them stored in the struct:
```
memory_base arena_start() current_point first_uncommitted_page arena_address_limit()
| | | | |
+---- Arena --------+---- live --------+---- committed ----+------ reserved --------+
| header | allocations | slack | address space only |
```
| Boundary | Meaning |
|---|---|
| `memory_base` | Start of the reservation. Null exactly when the arena is uninitialized or its reservation failed. |
| `arena_start()` | First allocatable byte. Always `memory_base + size_of(Arena)`, rounded up to `ARENA_DEFAULT_ALIGNMENT`. |
| `current_point` | Bump pointer. Everything below it has been handed out. |
| `first_uncommitted_page` | End of physically backed memory. Always page-aligned. |
| `arena_address_limit()` | End of the reservation. |
The invariant chain is
`memory_base <= arena_start() <= current_point <= first_uncommitted_page <= arena_address_limit()`.
Reserving is cheap — it consumes address space, not memory — so picking a generous reserve class
costs little on a 64-bit target. Committing is what has a real cost, and only happens as
allocations demand it.
---
## Quick start
An arena you own, with the struct on the stack or in your own data:
```jai
a: Arena;
success := arena_init(*a, .Size_64M);
if !success return;
defer arena_delete(*a);
buffer := arena_alloc(*a, 4096);
{
push_arena(*a); // context.allocator for this scope
nodes := NewArray(1024, Node);
}
```
An arena that owns itself, with the header stored in its own first page:
```jai
a := bootstrap_arena(.Size_2M, label = "frame");
if !a return;
defer arena_delete(a); // `a` dangles after this -- see Lifecycle
```
Scoped reclamation:
```jai
{
auto_reset(*a); // rewind to here on scope exit, keep the pages
scratch := arena_alloc(*a, 1024 * 1024);
...
}
```
---
## Constants and types
### Module parameter
`ARENA_DEBUG` (default `false`) adds `high_water_mark`, `label`, `file_path`, `function_name`
and `line_number` to `Arena`, and makes `update_high_water_mark` do work. It grows
`size_of(Arena)`, which moves `arena_start()`. See
[Correctness notes](#correctness-notes-and-limitations).
### Constants
| Name | Value | Notes |
|---|---|---|
| `ARENA_DEFAULT_COMMIT_PAGE_COUNT` | `16` | Counted in the arena's own pages: 64 KB normally, 32 MB on a 2 MB-page arena. |
| `ARENA_DEFAULT_ALIGNMENT` | `8` | Also the alignment `arena_start` rounds to. |
| `PLATFORM_MEMORY_PAGE_SIZE` | `4096` | The default granularity. A `.Large_Pages` arena uses `platform_large_page_size()` instead; read the arena's own size with `arena_page_size()`. |
| `PLATFORM_MEMORY_PAGE_SIZE_LOG2` | `12` | The `page_size_log2` an arena starts at. |
| `PLATFORM_CACHE_LINE_SIZE` | `64` | Declared, unused. |
| `Arena_Sizes` | `[7] s64` | The reserve table, indexed by `Arena_Reserve`. |
`#run assert(Arena_Sizes.count == type_info(Arena_Reserve).values.count)` keeps the table and
the enum from drifting; adding a class to one and not the other fails the build.
### `Arena_Reserve`
`enum u8`, one member per reserve class: `Size_64K`, `Size_2M`, `Size_64M`, `Size_2G`,
`Size_64G`, `Size_2T`, `Size_64T`. The value is an index into `Arena_Sizes`.
The three largest classes are 64-bit only in practice, and can fail on a Linux host configured
with `overcommit_memory=2` — see [Platform layer](#platform-layer).
### `Arena_Flags`
`enum_flags u8`. `Is_Bootstrapped`, `Large_Pages` and `Secure_Arena` are live and are passed to
`arena_init` or `bootstrap_arena`. `Chained` is declared so the bit layout is settled before
anything persists an `Arena`, but setting it has no effect; it is marked `#Planned` in the
source.
| Flag | Effect |
|---|---|
| `Is_Bootstrapped` | Set by `arena_set_bootstrap_flag`; the `Arena` lives in its own first page. |
| `Large_Pages` | Back the reservation with the platform's large/huge pages. A hint: see [Large pages](#large-pages). |
| `Secure_Arena` | Lock each range into physical memory as it is committed. A hint in the same sense. |
| `Chained` | `#Planned`, no effect. |
### `Arena`
```jai
Arena :: struct {
current_point: *u8;
memory_base: *u8;
first_uncommitted_page: *u8;
reserve: Arena_Reserve; // enum u8
flags: Arena_Flags; // enum_flags u8
status: Arena_Status; // enum_flags u8
alignment_log2: u8;
page_size_log2: u8 = PLATFORM_MEMORY_PAGE_SIZE_LOG2;
initial_commit_page_count_log2: u8;
}
```
32 bytes when `ARENA_DEBUG` is off. Keeping it small is deliberate: the header is carved out of
the front of every arena's own reservation, so each byte added comes out of usable space. Three
pointers and six single-byte fields is 30 bytes, rounding to 32 at pointer alignment, so two
bytes are spare and one field wider than a byte would push it to 40. That is what the log2s
buy — alignment, page size and the initial commit count each fit in a byte. The test suite
asserts the size for this reason: if it fires, that is a decision to make rather than a number
to update.
**Read the three counts through their accessors**, never off the field:
| Accessor | Returns |
|---|---|
| `arena_alignment(arena)` | The alignment every allocation is rounded up to. |
| `arena_page_size(arena)` | This arena's page granularity. |
| `arena_initial_commit_pages(arena)` | The initial commit, in this arena's pages, and the floor `arena_release` decommits down to. |
Because they are log2s, all three are necessarily powers of two — which is why `arena_init`
rounds a requested `commit_page_count` up to one, and why `arena_set_alignment` rejects
non-power-of-two alignments rather than rounding them.
### `Arena_Status`
`enum_flags u8`, recording what actually happened to the arena as opposed to what the caller
asked for. Both bits are sticky and never cleared.
| Flag | Meaning |
|---|---|
| `Secure_Lock_Failed` | A `.Secure_Arena` page lock was refused. A range that spent time unlocked cannot be made retroactively safe by a later success. |
| `Large_Pages_Failed` | A `.Large_Pages` request fell back to `PLATFORM_MEMORY_PAGE_SIZE`. Pages already handed out at 4 KB granularity do not become large ones later. |
It is a separate field from `Arena_Flags` rather than two of that enum's four free bits, and not
for size reasons — both layouts land at 32 bytes. `arena_init` merges caller-supplied flags into
`flags`, so an outcome living there would be settable by a caller, which is not an outcome.
Read the bits through `arena_secure_lock_intact` and `arena_large_pages_active`, which also
check that the corresponding capability was requested at all.
---
## Lifecycle
### `arena_init`
```jai
arena_init :: (arena: *Arena, new_reserve: Arena_Reserve,
commit_page_count := ARENA_DEFAULT_COMMIT_PAGE_COUNT,
new_flags: Arena_Flags = .None)
-> (success: bool, pages_committed: s64 = 0)
```
`new_flags` is merged into `arena.flags` before the reservation happens, so `.Large_Pages`
reaches the reserve call and `.Secure_Arena` covers the first commit as well as every later one.
Reserves address space, commits the first `commit_page_count` pages, and establishes every
invariant above. `pages_committed` reports what was actually committed, which is not always
what you asked for:
| Request | Result |
|---|---|
| Negative | Clamped to `ARENA_DEFAULT_COMMIT_PAGE_COUNT`, warned. |
| Zero | Clamped to `ARENA_DEFAULT_COMMIT_PAGE_COUNT`, warned. |
| Not a power of two | Rounded **up** to one, silently. |
| Larger than the reserve | Clamped to the reserve, warned. |
| Commit fails | `pages_committed` is 0; `success` is still `true`. |
The rounding is what lets the count be stored in a byte as a log2. Up rather than down, so the
initial commit is never smaller than asked for; every other outcome in the table already lands
on a power of two, since `ARENA_DEFAULT_COMMIT_PAGE_COUNT` is one and a reserve class divided
by a page size is one. `arena_commit_first_pages` is *not* rounded — a direct caller passing
`start_offset_bytes` can still produce a non-power-of-two commit, and that path does not write
the field.
`success` reports only that the *reservation* worked. An arena can come back with
`success == true` and `pages_committed == 0`, which is usable — allocation will commit on
demand — but is not enough for a bootstrapped arena, whose header needs a page. Check both if
you care.
Calling it on an arena that already has a `memory_base` is a no-op returning
`(true, arena_initial_commit_pages(arena))`. It will not re-reserve, and it will not change the reserve
class. `null` returns `(false, 0)` with an error logged.
### `bootstrap_arena`
```jai
bootstrap_arena :: (new_reserve: Arena_Reserve,
default_commit_page_count: s32 = ARENA_DEFAULT_COMMIT_PAGE_COUNT,
label := "", loc := #caller_location) -> *Arena
```
Creates an arena whose `Arena` struct lives in its own first page, so nothing else has to own
it. Returns `null` if the reservation fails, or if it succeeds with no pages behind it.
`label` and `loc` are recorded only under `ARENA_DEBUG`; the strings are copied with
`context.default_allocator` and freed by `arena_delete`.
**The pointer dangles after `arena_delete`.** The struct is inside the region being unmapped,
so it cannot be nulled for you. Do not call `is_valid` or `arena_details` on it afterwards —
both dereference and will fault rather than report anything. Null your own copy, and make sure
no other copy outlives the delete.
### `arena_delete`
```jai
arena_delete :: (arena: *Arena)
```
Releases the whole reservation (`MEM_RELEASE` / `munmap`) and, under `ARENA_DEBUG`, frees the
recorded strings. A no-op if `!is_valid(arena)`. Failures are logged and the procedure returns
without clearing `memory_base`, so the struct never claims to have released something still
mapped.
For a **non-bootstrapped** arena it clears `memory_base`, making `is_valid` false and leaving
the struct ready to be initialized again. For a **bootstrapped** arena it cannot, per above.
### `is_valid`
```jai
is_valid :: (arena: *Arena) -> bool
```
`arena != null && arena.memory_base != null`. Safe on a null pointer; **not** safe on a
bootstrapped arena that has been deleted.
---
## Allocation
### `arena_alloc`
```jai
arena_alloc :: (arena: *Arena, byte_count: s64) -> *void
```
Aligns `current_point` up to `arena_alignment(arena)`, reserves `byte_count` bytes, commits any pages
needed, and advances. Returns `null` on every failure, without moving `current_point`:
| Condition | Behaviour |
|---|---|
| `arena` is null | Logs, returns `null`. |
| `arena.memory_base` is null | Logs, self-initializes with a **`.Size_64G`** reserve, then proceeds. |
| `byte_count < 0` | Logs, returns `null`. |
| Request exceeds the reservation | Logs, returns `null`. |
| Commit fails | Returns `null`. |
`byte_count == 0` is legal and returns the current aligned point.
The self-initializing path is a safety net, not a feature. It logs at error level and picks a
64 GB reserve because address space is cheap and a wrong guess here is hard to recover from;
initialize your arenas properly.
The bounds test is written as a subtraction against the limit
(`byte_count > arena_address_limit(arena) - result`) so it cannot overflow, which a
`result + byte_count` form could.
---
## Alignment
Every allocation is aligned to `arena_alignment(arena)`, which starts at `ARENA_DEFAULT_ALIGNMENT`
(8). The Jai allocator interface carries no per-allocation alignment, so this is the only
control available.
### `arena_set_alignment`
```jai
arena_set_alignment :: inline (arena: *Arena, alignment: s64)
```
Must be a power of two no wider than the reservation. Non-positive or non-power-of-two values
are asserted in debug, logged and ignored in release. Values above `reserve_size_bytes(arena)`
are clamped to it with an error, because an alignment wider than the whole reservation makes
every allocation fail the address-limit check.
The reservation is the only bound. This used to clamp at 32768 because the field was a `u16`
and anything larger truncated to zero; the field is a log2 now, so a 2 MB alignment on a large
enough arena is exact.
This is the single place the field is written; everything else routes through it.
### `push_alignment`
```jai
push_alignment :: (arena: *Arena, alignment: s64) #expand
```
Sets the alignment for the enclosing scope and restores the previous value on exit via a
`defer`. Nests correctly. An invalid alignment is rejected before anything is changed.
Use this for types the default 8 does not satisfy — anything holding `__m128`/`__m256`, or a
value the compiler assumes is naturally aligned:
```jai
{
push_alignment(*a, 32);
vectors := NewArray(count, Vector8);
}
```
---
## Resetting and releasing
All four are no-ops on an invalid arena.
| Procedure | Rewinds | Decommits |
|---|---|---|
| `arena_reset(arena)` | yes | no |
| `arena_reset_keeping_memory(arena)` | yes | no |
| `arena_release(arena)` | yes | down to `arena_initial_commit_pages(arena)` |
| `arena_reset_overwriting_memory(arena, $wipe_function)` | yes, after wiping | no |
`arena_reset` and `arena_reset_keeping_memory` are currently identical; the latter is a
compatibility spelling.
Reset is the cheap one: the pages stay committed, so the next round of allocation does no
system calls. Release hands pages back to the OS and is worth it only when the arena has grown
far past its steady state and will stay there.
`arena_reset_overwriting_memory` zeroes everything between `arena_start()` and `current_point`
before rewinding. `$wipe_function` defaults to `memory_secure_overwrite`; passing `null`
skips the wipe.
Rewinding does not invalidate pointers in any enforced sense — the memory stays mapped and
readable — but every pointer into the rewound region is logically dead and will be handed out
again by the next allocation.
---
## Scoped helpers
```jai
auto_reset (arena: *Arena) #expand
auto_release (arena: *Arena) #expand
auto_reset_overwriting_memory (arena: *Arena, $wipe_function := memory_secure_overwrite) #expand
auto_release_overwriting_memory(arena: *Arena, $wipe_function := memory_secure_overwrite) #expand
```
Each captures `current_point` and installs a `defer` restoring it at the end of the enclosing
scope. The `release` variants also decommit back down to `arena_initial_commit_pages(arena)`; the
`overwriting` variants wipe the region between the mark and the current point first.
```jai
process :: (a: *Arena, input: [] u8) -> Result {
auto_reset(a); // everything below is scratch
table := arena_alloc(a, 1 << 20);
...
return copy_out(result); // copy anything that must outlive the scope
}
```
They nest, and unwind in reverse order. The underlying `Arena_State`, `get_arena_state` and
`restore_arena_state` are `#scope_module` deliberately: a mark and its restore must refer to
the same arena, and keeping the pair internal means an outside caller cannot pair them wrongly.
### `push_arena`
```jai
push_arena :: (arena: *Arena) #expand
```
Installs the arena as `context.allocator` for the enclosing scope and restores the previous
allocator on exit. Combines with `auto_reset` to get both a scoped allocator and scoped
reclamation.
---
## Allocator integration
### `get_allocator`
```jai
get_allocator :: (arena: *Arena) -> Allocator
```
Returns `.{arena_allocator_proc, arena}` for passing to anything that takes an `Allocator`.
### `arena_allocator_proc`
The standard `Allocator_Proc`. Behaviour by mode:
| Mode | Behaviour |
|---|---|
| `ALLOCATE` | `arena_alloc`. Returns `null` on failure. |
| `RESIZE` | Always allocates fresh and copies `min(old_size, requested_size)` bytes. Never resizes in place. |
| `FREE` | Ignored, returns `null`. |
| `IS_THIS_YOURS` | True only for `arena_start() <= p < current_point`, i.e. currently live. |
| `CAPS` | `IS_THIS_YOURS \| HINT_I_AM_A_FAST_BUMP_ALLOCATOR`, and fills the name string. |
| `STARTUP`, `SHUTDOWN`, `THREAD_START`, `THREAD_STOP`, `CREATE_HEAP`, `DESTROY_HEAP` | Accepted, return `null`. |
Every mode tolerates a null `allocator_data`. This matters: `Basic.get_capabilities` passes
`null` deliberately, since capabilities describe the proc rather than any one heap.
`IS_THIS_YOURS` answers about the *live* range, so a pointer into committed-but-unallocated
slack is disowned, and so is everything after a reset.
The advertised capabilities are deliberately narrow. `FREE` is absent because `.FREE` does
nothing; `ACTUALLY_RESIZE` is absent because resize always reallocates; `MULTIPLE_THREADS` is
absent because nothing here is synchronized.
---
## Secure erase and page locking
### `arena_wipe`
```jai
arena_wipe :: (start: *u8, end: *u8, $wipe_function := memory_secure_overwrite) #expand
```
Zeroes the half-open interval `[start, end)`. **Returns without doing anything if
`end <= start`.** Every wipe in the module routes through this, which is what keeps a negative
length away from `memory_secure_overwrite` — on x64 that is a `rep stosb` with the count in
`rcx`, so a negative `s64` would become a wipe of roughly the whole address space.
`memory_secure_overwrite` now lives in the `Secure_Memory` module rather than in Basic, and
that module's `USE_SODIUM` defaults to true, so the wipe goes through libsodium's
`sodium_memzero` and the assembly above is the fallback for a build that opts out.
`#if wipe_function` means passing `null` compiles the call away entirely.
### `arena_secure_overwrite`
```jai
arena_secure_overwrite :: (arena: *Arena, $endpoint := Arena_Secure_Overwrite_Endpoint.Current_Point)
```
Zeroes from `arena_start()` to either `current_point` (everything handed out) or
`first_uncommitted_page` (that plus the committed slack). `$endpoint` is a constant, resolved
with `#if`.
Neither moves `current_point`. To wipe *and* rewind, use `arena_reset_overwriting_memory`.
### `arena_secure_overwrite_starting_from`
```jai
arena_secure_overwrite_starting_from :: (arena: *Arena, start: *u8)
```
Zeroes from `start` to `first_uncommitted_page`. An out-of-range `start` is a no-op via
`arena_wipe`'s guard rather than an underflow.
### `arena_lock_pages` / `arena_unlock_pages`
```jai
arena_lock_pages :: (arena: *Arena)
arena_unlock_pages :: (arena: *Arena)
```
`VirtualLock` / `mlock` over the committed region, keeping it out of the page file. Failures
are logged, not asserted — these legitimately fail against a small working-set quota or
`RLIMIT_MEMLOCK`, so treat success as best-effort and check your logs if it matters.
Locking covers `memory_base` through `first_uncommitted_page`. Pages committed *after* a lock
are not covered; lock again if the arena has grown.
---
## Introspection
| Procedure | Returns |
|---|---|
| `arena_start(arena)` | First allocatable address. |
| `arena_address_limit(arena)` | End of the reservation. |
| `arena_usage_bytes(arena)` | `current_point - arena_start()`. |
| `arena_committed_bytes(arena)` | `first_uncommitted_page - memory_base`. Includes the header. |
| `reserve_size_bytes(arena)` / `(Arena_Reserve)` | Size of the reserve class. |
| `arena_is_bootstrapped(arena)` | Whether the header lives inside the arena. |
| `arena_details(arena)` | Human-readable summary. Safe on `null`. |
| `next_reserve(size, alignment)` | Smallest class that can hold `size` bytes of user data. |
Note the asymmetry: `arena_usage_bytes` measures from `arena_start()` and so excludes the
header, while `arena_committed_bytes` measures from `memory_base` and includes it. Usage will
never equal committed even on a completely full arena.
`next_reserve` accounts for the header and alignment padding, so `next_reserve(64 * 1024, 8)`
returns `.Size_2M` — a 64 KB class cannot hold 64 KB of user data once the header is carved
out. It saturates at `.Size_64T`.
`arena_details` returns a string allocated from temporary storage. It is a debugging aid; do
not hold on to the result.
---
## Platform layer
Exported from `arena_platform.jai`. Most callers never need these — `arena_init` and
`arena_alloc` drive them — but they are public and the test suite exercises them directly.
| Procedure | Behaviour |
|---|---|
| `arena_platform_init(arena, reserve) -> bool` | Reserves address space, and sets `page_size_log2` and `first_uncommitted_page` alongside `memory_base`. **Postcondition: `memory_base` is null if and only if this returned false.** |
| `extend_committed_pages(arena, end) -> bool` | Commits through `end`, rounded up to one of the arena's pages. Returns `true` without doing anything if `end` is already committed. |
| `platform_large_page_size() -> s64` | The machine's large page size, or `0` where unavailable. Cached. See [Large pages](#large-pages). |
| `arena_commit_first_pages(arena, commit_size, start_offset_bytes = 0) -> bytes_committed` | Commits the initial run. Clamps as described under `arena_init`. Returns the true page-aligned figure. |
| `Align(n, alignment) -> n` | Rounds up. Asserts the alignment is a positive power of two. Works on integers and pointers. |
`free_pages_down_to` is `#scope_module`. It decommits everything above a page count without
consulting `current_point`, which is safe only because its two callers (`arena_release` and
`restore_arena_state`) rewind first. Keeping it internal is what makes that safe rather than
merely conventional.
### Platform differences worth knowing
**Decommit is not symmetric.** Windows uses `MEM_DECOMMIT`, so reading a stale pointer into a
decommitted page faults. POSIX uses `madvise(MADV_DONTNEED)`, which leaves the pages mapped and
reading back as zeros. A use-after-release bug is therefore loud on Windows and silent on
Linux and macOS. Test that class of bug on Windows.
**POSIX commits eagerly.** The `mmap` in `arena_platform_init` maps the whole reservation
`PROT_READ | PROT_WRITE`, so `extend_committed_pages` there is bookkeeping only and cannot
fail. No `MAP_NORESERVE` is passed, so on a host configured with `overcommit_memory=2` the
large reserve classes will fail at reservation time.
**A granted large-page reservation is committed eagerly everywhere.** On Windows and macOS it
is wired at reserve time and cannot be partially given back, which makes both
`extend_committed_pages` and `free_pages_down_to` pure bookkeeping for such an arena. Linux
`MAP_HUGETLB` keeps the normal decommit behaviour, at huge-page granularity. See
[Large pages](#large-pages).
**Failure is reported, not asserted.** Every `VirtualAlloc`, `VirtualFree`, `mmap` and `munmap`
result is checked and logged, and the failure propagates to the caller as `false` or `null`.
Nothing in the commit path relies on an assert that a release build would strip.
---
## Thread safety
**None. This module is not thread-safe in any respect.**
There is no lock, no atomic, and no per-thread state anywhere in either file. `arena_alloc`
performs an unsynchronized read-modify-write on `current_point`, so two threads allocating
from one arena will hand out overlapping memory. `.CAPS` correctly omits
`Allocator_Caps.MULTIPLE_THREADS`, and `THREAD_START` / `THREAD_STOP` are accepted as no-ops
rather than being used to build per-thread state.
The dangerous part is that nothing stops you. `push_arena` installs an arena as
`context.allocator`, and a context is exactly the sort of thing that gets copied to a worker
thread. Every allocation on that thread then races.
Safe patterns:
- **One arena per thread.** The usual answer. Arenas are cheap; give each worker its own.
- **Hand off ownership.** One thread may fill an arena and pass it to another, provided there
is a real happens-before edge between them and only one thread touches it at a time.
- **Read-only sharing.** Multiple threads may read memory already allocated, as long as none
of them allocates, resets, or releases.
Unsafe, including cases that look benign:
- Two threads calling `arena_alloc` on one arena, however briefly.
- One thread allocating while another calls `arena_reset`, `arena_release`, or any `auto_*`
scope exit — the rewind can hand out live memory again.
- Sharing a context that carries an arena allocator.
- One thread reading arena memory while another resets or releases, which on Windows can fault
and on POSIX will silently read zeros.
If you need a shared allocator, wrap it yourself, and note that a lock around `arena_alloc`
alone is not enough — the reset and release paths need to be inside the same lock.
---
## Correctness notes and limitations
Known, accepted, or deferred. None of these are bugs pending a fix unless stated.
**Alignment tops out at the reservation.** Requests above `reserve_size_bytes(arena)` are
clamped with an error. This was a 32768 ceiling imposed by a `u16` field, which would have made
2 MB alignment inexpressible; the field is `alignment_log2: u8` now, so the constraint is the
meaningful one rather than a field width.
**The default alignment of 8 under-aligns some types.** The allocator interface has no
per-allocation alignment, so anything needing 16 or 32 bytes must be wrapped in
`push_alignment`. This differs from the default heap allocator's guarantee.
**Every arena pays for the header.** `arena_start()` skips `size_of(Arena)` whether or not the
struct is stored there. For a non-bootstrapped arena those bytes are simply never written.
Accepted: it keeps one address layout for both kinds, and the cost is 32 bytes against a
reservation of megabytes at minimum.
Branching on `arena_is_bootstrapped` to reclaim them is not rejected on cost — the test would be
a byte load and a `CMOV` off a cache line the caller already has. It is rejected because
`arena_start` has to stay a pure function of `memory_base`: callers compare two of its results
across an arena's life, and `flags` is mutable. It is also not set yet where it would be needed,
since `arena_init` runs before the arena is known to be bootstrapped at all.
**`arena_start()` moves between build configurations.** `ARENA_DEBUG` adds five fields, so
`size_of(Arena)` and therefore the first allocatable address differ between debug and release.
Never persist an offset into arena memory across a configuration change.
**`.RESIZE` does not check `old_memory`.** A caller passing a null `old_memory` with a non-zero
`old_size` will make the `memcpy` read from address zero. Callers are expected to pass a real
block, but the guard is one condition and is worth adding.
**Reset does not invalidate anything.** Memory below `current_point` stays mapped after a
rewind. Stale pointers read plausible data right up until the next allocation reuses the
bytes. The `overwriting` variants exist for when that matters.
**`Chained` does nothing.** It is declared to fix the bit layout; setting it has no effect, and
it is marked `#Planned` in the source. `Large_Pages` and `Secure_Arena` are live.
**`Large_Pages` and `Secure_Arena` are hints, not guarantees.** Both can be refused by the OS,
and a refusal produces a working arena with the capability missing rather than a failed
`arena_init`. Ask `arena_large_pages_active` or `arena_secure_lock_intact` if the answer
matters; a caller that never asks cannot tell the difference.
**A large-page arena does not return memory.** On Windows and macOS the reservation is
committed and wired in one call and cannot be partially decommitted, so `arena_release` and the
`free_pages` path of `restore_arena_state` rewind `current_point` and nothing more.
**`start_offset_bytes` has no in-tree caller.** `arena_commit_first_pages` implements and
guards it, and the test suite covers it, but no production code exercises it. Treat it as
provisional until `Arena_Array` exists.
**`arena_alloc`'s self-initializing path picks 64 GB.** Reaching it means an uninitialized
arena escaped into allocation. It logs at error level. Do not rely on it.
**Large reserve classes are not universally available.** `Size_2T` and `Size_64T` need a
64-bit address space, and on Linux with strict overcommit even `Size_64G` can fail. The failure
is clean — `arena_init` returns `false` — but plan for it if you use the top of the table.
---
## Large pages
*`Arena_Flags.Large_Pages`*
Backing an arena with the platform's large/huge pages — 2 MB in practice — instead of 4 KB, to
cut TLB pressure on large working sets. Pass the flag to `arena_init` or `bootstrap_arena`:
```jai
arena := bootstrap_arena(.Size_64M, flags = .Large_Pages, label = "big working set");
if !arena_large_pages_active(arena) {
// Still a perfectly good 4 KB arena. Only worth a log if the TLB win was the point.
}
```
| Procedure | Meaning |
|---|---|
| `platform_large_page_size() -> s64` | The machine's large page size, or `0` where unavailable. Cached after the first call. |
| `arena_is_large_pages(arena) -> bool` | The flag was requested. Says nothing about whether it was granted. |
| `arena_large_pages_active(arena) -> bool` | Requested **and** granted. This is the one to test. |
| `arena_page_size(arena) -> s64` | This arena's granularity: `platform_large_page_size()` when active, `PLATFORM_MEMORY_PAGE_SIZE` otherwise. |
The granularity is per-arena, held as a `page_size_log2: u8`, so 4 KB and large-page arenas
coexist in one process and a refusal downgrades one arena rather than invalidating a build-wide
assumption. Every page calculation in both files runs off `arena_page_size` / `page_size_log2`;
`PLATFORM_MEMORY_PAGE_SIZE` is now only the default.
**Requesting it is a hint.** A request is refused when the machine reports no large page size,
when the reservation cannot hold a whole number of large pages (`Size_64K`), or when the OS
declines the call. Any of those logs a warning, sets the sticky `Arena_Status.Large_Pages_Failed`,
and continues with a working 4 KB arena. There is deliberately **no ceiling on the reserve class** —
a caller asking for large pages on a large reservation gets what it asked for.
**Windows.** One `VirtualAlloc` with `MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES`, because
`MEM_LARGE_PAGES` has no reserve-only form. The whole reservation is therefore committed and
wired immediately, `first_uncommitted_page` starts at the top, and `free_pages_down_to` returns
early — a large-page allocation cannot be partially decommitted. It also needs
`SeLockMemoryPrivilege`, which is not granted by default; acquiring it is the main program's
job (`windows_enable_large_pages_if_possible` in the app's platform layer), and without it this
module simply falls back.
**Linux.** `MAP_HUGETLB` first, sized against `Hugepagesize:` from `/proc/meminfo`. It draws
from a hugetlbfs pool an administrator preallocated with `vm.nr_hugepages`, so on a default box
it fails — and then the plain mapping is retried with `madvise(MADV_HUGEPAGE)` behind it.
Transparent huge pages are advisory, so that path reports `arena_large_pages_active` as false
and keeps 4 KB arithmetic, but costs one syscall and may recover most of the win. `MAP_HUGETLB`
pages *can* be dropped with `madvise`, at huge-page granularity, so decommit still works there.
**macOS.** `VM_FLAGS_SUPERPAGE_SIZE_2MB` passed in `mmap`'s `fd` argument, x86_64 only —
Apple Silicon exposes no user-facing superpage API, so `platform_large_page_size` reports `0`
there. Superpages are wired at map time like Windows large pages, with the same consequence for
`free_pages_down_to`.
**Interaction with `.Secure_Arena`.** Large pages are non-pageable by construction, so an arena
with both flags locks nothing: `arena_manages_page_locks` is false, the per-commit
`VirtualLock`/`mlock` is skipped, and `arena_lock_pages` / `arena_unlock_pages` report success
without a syscall. The residency guarantee holds either way, and skipping it avoids both a
syscall on the allocation path and a spurious `Arena_Status.Secure_Lock_Failed`.
**Allocation alignment is a separate axis.** Page granularity and `arena_set_alignment` are
unrelated — large pages need no oversized allocation alignment. A 2 MB *allocation* alignment
used to be inexpressible because `Arena.alignment` was a `u16`; it is `alignment_log2: u8` now,
bounded only by the reservation, so both work independently.
---
## Secure arenas
*`Arena_Flags.Secure_Arena`*
Locking an arena's pages into physical memory so its contents cannot reach the page file or a
hibernation image. Pass the flag the same way:
```jai
arena := bootstrap_arena(.Size_64M, flags = .Secure_Arena, label = "vault");
```
| Procedure | Meaning |
|---|---|
| `arena_is_secure(arena) -> bool` | The flag was requested. |
| `arena_secure_lock_intact(arena) -> bool` | Requested, and every lock the arena took succeeded. |
| `arena_lock_pages(arena) -> bool` | One-shot lock of everything committed. Rarely needed directly. |
| `arena_unlock_pages(arena) -> bool` | The inverse; called at teardown. |
Each range is locked as it is **committed**, not once at init, because a lock taken at init
covers only the initial commit and leaves everything the arena later grows into unlocked. The
decommit path unlocks first, both to keep the process's locked-page accounting honest on
Windows and because `madvise(MADV_DONTNEED)` would otherwise refuse a locked range on Linux.
A failed lock is recorded in the sticky `Arena_Status.Secure_Lock_Failed` and never propagated: a low
`RLIMIT_MEMLOCK` or working-set quota must not turn into an out-of-memory. The quota is the
caller's to arrange before creating the arena — see `os_request_memory_lock_quota` in the app's
platform layer.
See [Large pages](#large-pages) for why the two flags together lock nothing.
**Still open.** Wiping on every reset and release rather than only through the `overwriting`
variants; wiping before `arena_delete` hands the reservation back; and whether `push_arena` on
a secure arena should be loud or refused, since routing general-purpose allocation into it
defeats the purpose.
---
## Planned features
One feature has a hook in the source but no behaviour: `Arena_Flags.Chained` is declared so the
bit layout is settled before anything persists an `Arena`, and is marked `#Planned` where it is
declared. Setting it today does nothing.
What follows is what it would take, recorded now so the reasoning does not have to be
rediscovered later.
### Expandable (chained) arenas
*`Arena_Flags.Chained` · `#TODO: Consider how to approach ExpandableArenas`*
An arena that links a fresh block when the current one runs out, instead of refusing the
allocation.
The obstacle is that **one reservation is a load-bearing assumption**, not an implementation
detail. Because there is exactly one block that never moves, `arena_address_limit()` is a
single address, `arena_usage_bytes` is one subtraction, `IS_THIS_YOURS` is one range test in
constant time, and every pointer stays valid for the arena's whole life. Chaining costs all
four:
- `arena_address_limit()` stops having a single meaningful value.
- `arena_usage_bytes` and `arena_committed_bytes` must sum across the chain.
- `IS_THIS_YOURS` must walk it, and stops being O(1).
- `Arena_State` marks become a (block, point) pair rather than one pointer, and
`restore_arena_state` must free every block allocated after the mark.
- `arena_release` and `free_pages_down_to` need a per-block policy: keep the first block's
initial commit and drop the rest, presumably.
- Every block carries a header, so the per-arena overhead becomes per-block.
**Decide first:** whether a chained arena is the same type or a separate one. Making `Arena`
chainable puts a branch in every accessor and taxes the common non-chained path; a distinct
type keeps the simple case simple and duplicates the API surface.
**Consider whether it is needed.** A reservation is address space, not memory, so on a 64-bit
target raising the reserve class is usually the cheaper answer to "this might need to grow" —
`Size_64G` costs nothing until it is committed. Chaining earns its keep when address space
itself is scarce, or when many arenas must each stay small.
---
## Testing
`tests/arena_tests.jai` covers every exported symbol of both files across ten groups, each
behind a `RUN_*_TESTS` constant so groups can be enabled individually. The module-scope
machinery is exercised through its public wrappers: `free_pages_down_to` via `arena_release`,
and the `Arena_State` pair via the `auto_*` macros.
Two conventions worth knowing before reading the output:
- Tests that drive failure paths deliberately log. Those are bracketed by
`(expecting logged errors: ...)` banners — noise inside a banner is the test working.
- `RUN_ASSERT_TRIP_TESTS` is off by default. Those cases violate preconditions that are
enforced by `assert`, so with asserts on they abort the process rather than failing a check.
Enable them only in a build with `ENABLE_ASSERT = false`, to confirm the release-mode
fallbacks log and return rather than proceeding.
The suite reports per group rather than as one total. A check is one `expect` call reached at
runtime, not one distinct property, so a check inside a loop counts once per iteration — the
breakdown makes it visible where coverage actually sits.

View File

@ -0,0 +1,472 @@
# FixedArena
A bump allocator over one block that is allocated up front, fully committed, and used as-is.
`fixed_arena.jai`, no platform layer.
- [Model](#model)
- [Choosing between FixedArena and Arena](#choosing-between-fixedarena-and-arena)
- [Quick start](#quick-start)
- [Constants and types](#constants-and-types)
- [Lifecycle](#lifecycle)
- [Allocation](#allocation)
- [Resizing](#resizing)
- [Resetting](#resetting)
- [Scoped helpers](#scoped-helpers)
- [Allocator integration](#allocator-integration)
- [Thread safety](#thread-safety)
- [Correctness notes and limitations](#correctness-notes-and-limitations)
- [Testing](#testing)
---
## Model
A `FixedArena` takes one block of memory, reserves its first bytes for a header, and hands out
the rest by advancing a cursor. It never grows. When the block is full, allocation returns
`null` — that is the normal, expected way for a fixed arena to fail, and callers are meant to
check for it.
Three regions describe an arena, separated by two boundaries:
```
memory.data fixed_arena_start() memory.data + cursor memory.data + memory.count
| | | |
+---- header --------+---- live allocations -----+------- unallocated --------+
| FIXED_ARENA_ | handed out | never handed out |
| HEADER_BYTES | | |
```
| Boundary | Meaning |
|---|---|
| `memory.data` | Start of the block. Null exactly when the arena is uninitialized. |
| `fixed_arena_start()` | First allocatable byte. Always `memory.data + FIXED_ARENA_HEADER_BYTES`. |
| `memory.data + cursor` | Bump position. Everything below it has been handed out. |
| `memory.data + memory.count` | End of the block. Nothing is ever handed out at or past this. |
The header bytes are reserved whether or not the arena is bootstrapped. For a bootstrapped
arena that is where the `FixedArena` struct actually lives; for one set up over a caller's
buffer those bytes go unused. Paying them either way buys a single address layout, so
`fixed_arena_start`, `IS_THIS_YOURS` and the reset path need no special case. `arena.jai` makes
the same trade for the same reason.
Individual allocations cannot be freed. You reclaim in bulk with `fixed_arena_reset`, or
scope it with `auto_reset`.
**Alignment padding comes out of the block.** Each allocation begins at the next
`FIXED_ARENA_DEFAULT_ALIGNMENT`-aligned address, and the bytes skipped to reach it are consumed
like any others. So the room actually left is *not* `memory.count - cursor` — that overstates it
by up to `FIXED_ARENA_DEFAULT_ALIGNMENT - 1` bytes. Ask `fixed_arena_available` instead.
**Allocations are not zeroed.** Zeroing the block on creation would touch every page up front,
and it would be a guarantee the first reset silently withdrew, since a reset does not re-zero.
Zero what you need yourself.
## Choosing between FixedArena and Arena
| | `FixedArena` | `Arena` |
|---|---|---|
| Backing memory | One block from an allocator, or a buffer you own | A large virtual reservation |
| Growth | None; full means `null` | Commits pages on demand up to the reserve |
| Platform calls | None | `VirtualAlloc` / `mmap` |
| Alignment | Fixed at `FIXED_ARENA_DEFAULT_ALIGNMENT` (16) | Per-arena, settable, default 8 |
| Reclaim | `fixed_arena_reset`, `auto_reset` | Reset, release, and the secure-erase variants |
| Struct size | 40 bytes | 32 bytes in release, larger under `ARENA_DEBUG` |
Reach for `FixedArena` when you know the budget up front, when you want the arena to sit inside
memory you already have (a static buffer, a slab from somewhere else), or when you cannot make
platform virtual-memory calls. Reach for `Arena` when the ceiling is unknown and you would
rather reserve big and commit lazily.
## Quick start
```jai
// Bootstrapped: the arena allocates its own block and lives inside it.
arena := bootstrap_fixed_arena(64 * 1024);
if !arena return; // too small, or the allocation failed
defer delete_fixed_arena(arena); // `arena` dangles after this -- see below
{
push_arena(arena); // context.allocator is the arena for this scope
auto_reset(arena); // and the cursor rewinds when the scope ends
data := NewArray(100, float32);
if !data.data return; // a full arena returns null; check it
}
```
```jai
// Over memory you already own. The struct lives outside the block.
backing: [8192] u8;
arena: FixedArena;
if !fixed_arena_init(*arena, backing) return;
p := fixed_arena_alloc(*arena, 256);
if !p return;
```
## Constants and types
### Constants
| Name | Value | Meaning |
|---|---|---|
| `FIXED_ARENA_DEFAULT_ALIGNMENT` | `16` | Every allocation is aligned to this, as an address. |
| `FIXED_ARENA_HEADER_BYTES` | `size_of(FixedArena)` | Reserved at the base of every block. |
| `FIXED_ARENA_MINIMUM_SIZE` | header + alignment | Smallest block either constructor accepts. |
Note that `FIXED_ARENA_DEFAULT_ALIGNMENT` is 16 while `ARENA_DEFAULT_ALIGNMENT` in `arena.jai`
is 8. The difference is deliberate but it matters: memory handed out by an `Arena` is only
8-aligned, so a `FixedArena` placed in it starts from a block whose base is not 16-aligned.
The allocator aligns the *address* it returns rather than the offset into the block, so this
works — but it is why the distinction exists.
### `FixedArena`
```jai
FixedArena :: struct {
memory: []u8;
cursor: s64;
allocator: Allocator;
}
```
| Field | Meaning |
|---|---|
| `memory` | The block. `data` is null exactly when the arena is uninitialized. |
| `cursor` | Offset of the bump position from `memory.data`. Starts at `FIXED_ARENA_HEADER_BYTES`. |
| `allocator` | The allocator that owns `memory`. Read by `delete_fixed_arena`; null when the caller owns the block. |
Do not write `cursor` directly. In particular `cursor = 0` is not a reset: for a bootstrapped
arena it lets the next allocation overwrite the struct describing the block being allocated
from. Use `fixed_arena_reset`.
## Lifecycle
### `bootstrap_fixed_arena`
```jai
bootstrap_fixed_arena :: (size: s64) -> *FixedArena
```
Allocates a `size`-byte block from `context.allocator` and puts the arena's own header at the
base of it, so nothing else has to own the struct.
`size` covers the header as well as the memory you get to hand out, so the usable capacity is
at most `size - FIXED_ARENA_HEADER_BYTES`, and less once alignment padding between allocations is
accounted for. `fixed_arena_available` reports the real figure at any moment.
Returns `null` if `size` is below `FIXED_ARENA_MINIMUM_SIZE`, or if the allocation fails. Both
log. There is no partial success.
**The pointer dangles after `delete_fixed_arena`.** The struct is inside the block being
released, so it cannot be nulled for you. Do not read any field afterwards — `is_valid`
included, since it dereferences and will fault rather than report anything useful. Null your
own copy, and make sure no other copy outlives the delete.
### `fixed_arena_init`
```jai
fixed_arena_init :: (arena: *FixedArena, memory: []u8,
allocator := Allocator.{}) -> bool
```
Sets an arena up over memory the caller already owns — a static array, a slab from another
arena — with the `FixedArena` struct outside the block rather than at its base.
`allocator` is the allocator that owns `memory`. Leave it at its default when the caller owns
the memory and nothing should ever free it: `delete_fixed_arena` then does nothing, which is
the correct answer for a static buffer.
Returns `false`, with an error logged, for a null `arena`, a null block, or a block below
`FIXED_ARENA_MINIMUM_SIZE`. A failed init leaves the arena invalid rather than half-set-up.
Unlike the bootstrapped case, the struct survives `delete_fixed_arena` and can be initialized
again and reused.
### `delete_fixed_arena`
```jai
delete_fixed_arena :: (arena: *FixedArena)
```
Releases the block through `arena.allocator` — the allocator that made it, which is not
necessarily `context.allocator` at the point of the call. Tolerates `null`.
Does nothing when `arena.allocator.proc` is null, which is the caller-owned-buffer case from
`fixed_arena_init`.
Do not call this inside a `push_arena(arena)` scope. It would work, but it reads as though the
arena frees itself, and what actually happens depends on which allocator is installed.
### `is_valid`
```jai
is_valid :: (arena: *FixedArena) -> bool
```
True when the arena has a block large enough to hold its header and a cursor inside that block.
False for `null`. Every allocating entry point checks this first, so you do not need to.
Not usable on a bootstrapped arena after `delete_fixed_arena`: it dereferences the pointer.
## Allocation
### `fixed_arena_alloc`
```jai
fixed_arena_alloc :: (arena: *FixedArena, byte_count: s64) -> *void
```
Bump-allocates `byte_count` bytes, aligned to `FIXED_ARENA_DEFAULT_ALIGNMENT`. Returns `null`
rather than a pointer past the end of the block.
| Condition | Result |
|---|---|
| Fits | Aligned pointer; cursor advances to the end of the allocation. |
| Does not fit | `null`, logged. **The cursor does not move**, so a retry with a smaller size is not penalised. |
| `byte_count` is 0 | A valid pointer. No memory is handed out; the cursor moves only by alignment padding. |
| `byte_count` is negative | `null`, logged. |
| `byte_count` near `S64_MAX` | `null`, logged. The capacity test is written so it cannot overflow. |
| `arena` is null or invalid | `null`, logged. |
A zero-byte request always succeeds, including against an exactly full arena and regardless of
where the block starts, because nothing is handed out and so there is nothing to align. It is
the one request `fixed_arena_available` does not describe. Worth knowing if you are checking
`alloc(0)` for null as a proxy for exhaustion — it is not one, in either direction.
### `fixed_arena_available`
```jai
fixed_arena_available :: (arena: *FixedArena) -> s64
```
The largest `byte_count` the next `fixed_arena_alloc` can satisfy.
**This is not `memory.count - cursor`.** The next allocation starts at the next aligned address,
and the padding to reach it comes out of the block, so the raw gap overstates what will fit by
up to `FIXED_ARENA_DEFAULT_ALIGNMENT - 1` bytes. Asking for the raw gap is the mistake this
function exists to prevent; it is what the test suite got wrong at three separate sites on its
first run.
Returns 0 for a null or invalid arena and for a block with no room left, so it is always safe to
compare against without a null check first.
A zero-byte request is exempt: it succeeds even when this reports 0.
```jai
// Fill the rest of the block exactly.
remaining := fixed_arena_available(arena);
p := fixed_arena_alloc(arena, remaining); // succeeds; cursor now equals memory.count
q := fixed_arena_alloc(arena, 1); // null
```
### `fixed_arena_start`
```jai
fixed_arena_start :: (arena: *FixedArena) -> *u8
```
The first allocatable byte: `memory.data + FIXED_ARENA_HEADER_BYTES`. This is where
`fixed_arena_reset` rewinds to, and the lower bound `IS_THIS_YOURS` tests against.
## Resizing
### `fixed_arena_resize`
```jai
fixed_arena_resize :: (arena: *FixedArena, old_memory: *void,
old_size: s64, requested_size: s64) -> *void
```
Resizes a block, extending or shrinking it **where it sits** when it is the most recent
allocation — that is, when `old_memory + old_size` is exactly the current cursor. Anything else
allocates a fresh block and copies `min(old_size, requested_size)` bytes.
The in-place case matters more here than it would for a growable arena. Array growth is the
dominant caller of `RESIZE`, and without it, growing an array to *N* elements consumes O(*N*²)
bytes of a block that by definition cannot grow.
Shrinking the most recent allocation reclaims the difference.
A `null` `old_memory` is legal — growing an array that has not allocated yet — and is treated
as a plain allocation. Returns `null` on a negative size, an invalid arena, or a request that
does not fit.
## Resetting
### `fixed_arena_reset`
```jai
fixed_arena_reset :: (arena: *FixedArena)
```
Hands the whole block back for reuse by rewinding the cursor to `fixed_arena_start` — **not to
offset zero**. The header bytes stay reserved, which is what stops the next allocation from
overwriting a bootstrapped arena's own struct.
Previously allocated memory is not overwritten, only forgotten. If it held anything sensitive,
wipe it yourself before resetting; unlike `arena.jai`, there is no
`fixed_arena_reset_overwriting_memory`.
Does nothing on a null or invalid arena.
## Scoped helpers
### `push_arena`
```jai
push_arena :: (arena: *FixedArena) #expand
```
Installs the arena as `context.allocator` for the enclosing scope and restores the previous
allocator on exit. Everything that allocates through the context — `New`, `NewArray`,
`array_add`, `String_Builder` — comes out of the arena.
It restores the allocator, **not the cursor**. A scope that allocates without an `auto_reset`
alongside consumes from the block for the life of the program.
### `auto_reset`
```jai
auto_reset :: (arena: *FixedArena) #expand
```
Records the cursor and restores it when the enclosing scope exits. Nests correctly. The usual
pairing is both macros together:
```jai
{
push_arena(arena);
auto_reset(arena);
...
}
```
The mark holds the allocation position and nothing else, matching how `Arena_State` is scoped
in `arena.jai`. Alignment and the backing allocator are configuration rather than position, and
restoring them would silently undo unrelated changes a caller had made.
## Allocator integration
### `get_allocator`
```jai
get_allocator :: (arena: *FixedArena) -> Allocator
```
Wraps the arena as an `Allocator` for passing to `,,` call sites, storing in a struct, or
installing manually.
### `fixed_arena_allocator_proc`
```jai
fixed_arena_allocator_proc :: (mode: Allocator_Mode, requested_size: s64, old_size: s64,
old_memory: *void, allocator_data: *void) -> *void
```
The `Allocator_Proc`. Every mode is handled explicitly, under `#complete`, so a mode added
upstream is a compile error here rather than a silent `null`.
| Mode | Behaviour |
|---|---|
| `.ALLOCATE` | `fixed_arena_alloc`. |
| `.RESIZE` | `fixed_arena_resize`. |
| `.FREE` | `null`. A bump allocator cannot release an individual allocation. |
| `.IS_THIS_YOURS` | True only for memory that is both **allocated by this arena and currently live** — between `fixed_arena_start` and the cursor. The header and the unallocated tail are not yours, and a reset un-owns everything it handed out. |
| `.CAPS` | `IS_THIS_YOURS \| HINT_I_AM_A_FAST_BUMP_ALLOCATOR`, and fills in the name. |
| `.STARTUP`, `.SHUTDOWN` | `null`. |
| `.THREAD_START`, `.THREAD_STOP` | `null`. No per-thread state is built. |
| `.CREATE_HEAP`, `.DESTROY_HEAP` | `null`. |
A null `allocator_data` is survivable in every mode. `get_capabilities` in `module.jai` passes
null outright, because capabilities are a property of the proc rather than of any one heap.
### Memory debugger
`Memory_Debugger` deliberately ignores this allocator, the same way it ignores the pool and
temporary allocators. Allocations from a `FixedArena` are not tracked, and an overrun here
would not be caught for you — the only allocation the debugger sees is the block
`bootstrap_fixed_arena` takes from `context.allocator`.
That is why every bound this allocator cares about is checked inside `fixed_arena.jai` rather
than left to tooling, and why `tests/fixed_arena_tests.jai` is the only regression net there is.
## Thread safety
**None. Nothing in this file is thread-safe.**
Allocation is an unsynchronized read-modify-write of `cursor`, so two threads allocating from
one `FixedArena` will hand out overlapping memory. `.CAPS` correctly omits
`Allocator_Caps.MULTIPLE_THREADS`, and `THREAD_START` / `THREAD_STOP` are accepted as no-ops
rather than being used to build per-thread state.
The dangerous part is that nothing stops you. `push_arena` installs the arena as
`context.allocator`, and a context is exactly the sort of thing that gets copied to a worker.
Safe patterns:
- **One arena per thread.** The usual answer.
- **Hand off ownership.** One thread fills an arena and passes it to another, provided there is
a real happens-before edge and only one thread touches it at a time.
- **Read-only sharing.** Multiple threads reading memory already allocated, as long as none of
them allocates or resets.
Unsafe, including cases that look benign:
- Two threads calling `fixed_arena_alloc` on one arena, however briefly.
- One thread allocating while another calls `fixed_arena_reset` or exits an `auto_reset` scope
— the rewind can hand out live memory again.
- Sharing a context that carries a fixed arena allocator.
A lock around the allocating calls alone is not enough. It has to cover everything that touches
the cursor: `fixed_arena_alloc`, `fixed_arena_resize`, `fixed_arena_reset`, `auto_reset`'s scope
exit, and the allocator proc.
## Correctness notes and limitations
- **A full arena returns `null`, it does not abort.** This is the normal failure mode and
callers are expected to handle it. Nothing in the file asserts on exhaustion.
- **Allocations are not zeroed**, and a reset does not zero either.
- **There is no secure-erase path.** `arena.jai` has `arena_wipe` and the
`*_overwriting_memory` variants; this file has no equivalent. Wipe sensitive memory yourself
before resetting.
- **`cursor` is an offset, not a pointer**, and the header bytes are reserved in both layouts.
Do not persist a cursor value across a change of build configuration — `size_of(FixedArena)`
is what sets the start, and a struct layout change moves it.
- **The bootstrapped pointer dangles after delete.** Covered above; it is the single easiest
way to misuse this file.
- **No alignment control.** Alignment is fixed at `FIXED_ARENA_DEFAULT_ALIGNMENT`. There is no
equivalent of `arena_set_alignment` or `push_alignment`. If you need a larger alignment for a
particular allocation, over-allocate and align by hand.
- **In-place resize only helps the most recent allocation.** Resizing anything buried under a
later allocation copies, and the original is stranded until the next reset.
## Testing
`tests/fixed_arena_tests.jai` covers every exported symbol across seven groups, each behind a
`RUN_*_TESTS` constant so groups can be enabled individually. The module-scope state machinery
(`Fixed_Arena_State` and its pair) is exercised through `auto_reset`.
Each test names the review finding it guards, so a failure points at the reasoning as well as
the line. The reasoning lives in `fixed-arena-code-review.html`.
Two conventions worth knowing before reading the output:
- Tests that drive failure paths deliberately log. Those are bracketed by
`(expecting logged errors: ...)` banners — noise inside a banner is the test working, noise
outside one is not.
- The suite reports per group rather than as one total. A check is one `expect` call reached at
runtime, not one distinct property, so a check inside a loop counts once per iteration.
The suite passes: 165 checks over seven groups. Its first run failed 8 of 152 checks, all of them the same defect and
all of them in the tests: three sites computed the room left as `memory.count - cursor` and so
asked for more than could fit. The allocator was right to refuse every time. What the failures
actually exposed was a missing piece of API — there was no way for a caller to ask how much
would fit — which is why `fixed_arena_available` exists and why the raw-gap calculation is
called out twice above. With that closed, and with a zero-byte request made independent of the
block's base address, the suite runs clean.
The cases most worth keeping if the file is ever trimmed are the boundary ones: allocating to
exact capacity and one byte past it, a zero-byte request against a full arena, and the
alignment group's deliberately 8-but-not-16-aligned block. Those are the three places where the
obvious implementation is wrong in a way that ordinary use does not reveal.

64
modules/Basic/enums.jai Normal file
View File

@ -0,0 +1,64 @@
// @Cleanup: Put a better version of this into the standard library.
enum_names :: ($t: Type) -> [] string {
info := type_info(t);
assert(info.type == .ENUM);
return info.names;
}
enum_values_as_s64 :: ($T: Type) -> [] s64 {
info := type_info(T);
assert(info.type == .ENUM);
return info.values;
}
enum_values_as_enum :: ($T: Type) -> [] T { // This is likely to be renamed "enum_values" eventually.
info := type_info(T);
assert(info.type == .ENUM);
result: [..] T;
result.allocator = temp;
array_reserve(*result, info.values.count);
for info.values array_add(*result, cast,no_check(T) it);
return result;
}
enum_highest_value :: ($t: Type) -> s64 {
_, high := enum_range_given_info(type_info(t));
return high;
}
enum_lowest_value :: ($t: Type) -> s64 {
low, _ := enum_range_given_info(type_info(t));
return low;
}
enum_range :: ($t: Type) -> (low: s64, high: s64) {
low, high := enum_range_given_info(type_info(t));
return low, high;
}
enum_range_given_info :: (info: *Type_Info_Enum) -> (low: s64, high: s64) { // @Cleanup: A safe version of this will return type_of(T.loose)
if info.values.count == 0 return 0, 0;
low := cast(s64) info.values[0];
high := low;
for info.values {
low = min(low, it);
high = max(high, it);
}
return low, high;
}
enum_count :: ($T: Type) -> (count: s32) #expand {
return cast(s32)type_info(T).values.count;
} @Utility
enum_increment :: (e: *$T) {
e.* += 1;
if cast(s64)e.* > enum_highest_value(T) {
e.* = xx enum_lowest_value(T);
}
}

View File

@ -0,0 +1,187 @@
// Although these all come from the Basic module. I've aliased Basic in this way
// to better illustrate where each function comes from and to improve searchability.
#import "Basic";
demo_command_line_arguments :: () {
args := get_command_line_arguments();
defer array_reset(*args); // get_command_line_arguments() creates a new array so we release it here. The strings themselves are not copies, however!
print("Number of command line arguments: %\n", args.count);
for args {
print("Argument %: %\n", it_index+1, it);
}
}
demo_context_stuff :: () {
my_assertion_handler :: (loc: Source_Code_Location, message: string) -> bool {
print("Assertion failed: %\n", message);
return false; // Don't break or exit.
}
// The default assertion handler simply prints the source location
// of the assertion and a message to dump to the console on stdout,
// then calls abort(). It is pretty trivial to hook the assertion
// handler to perform arbitrary functions. A less trivial, alternate
// assertion handler can be observed in jai's Debug module (which,
// among other things, dumps the current stack and spawns an
// interactive message box).
context.assertion_failed = my_assertion_handler;
// Unfortunately, due to current language constraints, a message must
// be explicitly be passed to assert() in order to get more context from
// the assertion without spawning the debugger and navigating to the
// source location. Passing a message at all is optionally, however.
assert(1 == 2, "1 == 2");
context.assertion_failed = Context_Base.{}.assertion_failed; // Switch this back to the default value.
// We can do the same with logging and allocation.
my_logger :: (message: string, data: *void, info: Log_Info) {
print("my_logger: %\n", message);
}
context.logger = my_logger;
log("Hello, Hamburger!\n");
// Note that the identifier string is before the message/format string in log
log("1 == 2 is %\n", 1 == 2);
context.logger = Context_Base.{}.logger; // Note that the default logger is essentailly a no-op. One may miss vital information if a non-default logger is not set!
my_allocator_proc :: (mode: Allocator_Mode, size: s64, old_size: s64, old_memory_pointer: *void, allocator_data: *void) -> *void {
print("Allocation:\n");
print("Mode: %\n", mode);
print("Size: %\n", size);
print("Old Size: %\n", old_size);
print("Old Memory Pointer: %\n", old_memory_pointer);
print("Allocator Data: %\n", allocator_data);
mem := context.default_allocator.proc(mode, size, old_size, old_memory_pointer, allocator_data);
if mode == .ALLOCATE || mode == .RESIZE
then print("Returning memory at: %\n", mem);
return mem;
}
my_allocator: Allocator;
my_allocator.proc = my_allocator_proc;
context.allocator = my_allocator;
{
mem := alloc(64);
mem = realloc(mem, 4096, 64);
free(mem);
}
context.allocator = Context_Base.{}.allocator;
}
// @TODO this stuff seems to better belong in the Reflection module.
// If/when that happens, this can also be moved to the appropriate
// example file.
demo_reflection_stuff :: () {
My_Struct :: struct {
a: int;
b: string;
c: float;
}
member_info := get_field(type_info(My_Struct), "b");
print("Type_Info_Struct_Member for b: %\n", member_info.*);
My_Enum :: enum {
APPLE;
BANANA;
OCARINA;
}
print("Enum info for My_Enum\n");
print("Names:\n");
names := enum_names(My_Enum);
for names print("%\n", it);
print("Values:\n");
values := enum_values_as_s64(My_Enum);
for values print("%\n", it);
low, high := enum_range(My_Enum);
print("Range: [%, %]\n", low, high);
}
demo_system_info_stuff :: () {
now := current_time_consensus();
print("Time in UTC: %\n", to_calendar(now, .UTC));
print("Time local: %\n", to_calendar(now, .LOCAL));
print("Working directory: ");
working_directory := get_working_directory();
print("%\n", working_directory);
/*
exec_path := get_path_of_running_executable();
print("Executable path: %\n", exec_path);
set_working_directory(exec_path);
*/
}
demo_print_stuff :: () {
// print_sequence("Test ", 1, " ", 32.0, "\n");
print("% % '%'\n", 1234, 1234.0, "1234");
print("Hex-formatted int: %\n", formatInt(12342345234, base=16));
print("Scientific-notation-formatted float: %, Decimal value: %\n", formatFloat(12342345234.0, mode=.SCIENTIFIC), 12342345234.0);
Vector :: struct {
x: float = 1.0;
y: float = 3.0;
z: float = 5.0;
}
v: Vector;
print("Compactly-formatted-struct %\n", v);
print("Exanded format: %\n", formatStruct(v, use_long_form_if_more_than_this_many_members=1));
// Reusing a print parameter more than once by specifying a 1-based index into the parameter list
print("%1 %2 %1\n", 5423, "Hello");
// sprint() allows us to quickly get the result of a print operation but as a string
// Similarly, tprint() returns the result as a string, but allocated from temporary storage
// instead of as a heap allocation (or whatever the current context.allocator allocates from).
sprinted_string := sprint("%", v);
defer free(sprinted_string);
print("sprint() result: %\n", sprinted_string);
// For more complex string creation, String_Builder may be more suitable.
builder: String_Builder;
append(*builder, "Hello, String_Builder!\n"); // appends the string as-is to the String_Builder's pools.
print_to_builder(*builder, "%\n", v); // Performs print-like formatting while appending to the builder.
builder_string := builder_to_string(*builder);
defer free(builder_string);
print("String_Builder result: %\n", builder_string);
}
main :: () {
time := seconds_since_init();
demo_command_line_arguments();
demo_context_stuff();
demo_reflection_stuff();
demo_system_info_stuff();
demo_print_stuff();
time_new := seconds_since_init();
print("Duration: %\n", time_new - time);
write_string("Done.\n");
}

View File

@ -0,0 +1,319 @@
//
// We import Basic with MEMORY_DEBUGGER=true to cause alloc(), free(), and realloc()
// to be hooked with routines that record allocations and frees. This is a layer
// built on top of allocators, so that allocators do not have to implement their own
// memory debugging if they don't want to, and also, we can make sure you free a chunk
// of memory on the same allocator you allocated it from, which is a cross-cutting concern
// that involves multiple allocators.
//
// You can look at this memory debugging code if you want; it's not too complicated.
// it lives in modules/Basic/Memory_Debugger.jai.
//
// Note that enabling the MEMORY_DEBUGGER will slow down your program by some amount
// relative to how many allocations there are. So we recommend only enabling
// MEMORY_DEBUGGER when you really are working on memory issues, and disabling it
// at other times.
//
#import "Basic"()(MEMORY_DEBUGGER=true, VISUALIZE_MEMORY_DEBUGGER=!IS_CROSS_COMPILING);
#import "Pool";
Status :: struct { // Here's an arbitrary stuff that just has some things in it.
name1: string;
name2: string;
number: float64;
}
fruits :: string.["Apple", "Blueberry", "Canteloupe", "Zebra Fruit"];
main :: () {
// Often in programs you want to allocate some global data at startup, and that data
// remains in use for the duration of runtime. The RAII people would then say you should
// "destruct" this data at the end of runtime, because it's no longer in use. But this
// wastes CPU cycles and adds complexity to the program; the operating system is about
// to bulk-free all your memory, so why would you put the computer through a bunch of
// extra pain of getting all sorts of pointers into cache just so you can free them
// before the operating system frees them?
//
// But even if one decides they don't want to do this, then one day we come along and want
// to profile our memory use and make sure there aren't leaks. And the tool doesn't know that
// this data is global, and reports it as a leak. When there is a lot of global data, this
// clouds the reports with things we don't care about, making it harder to see the actual
// leaks, and decreasing our quality of life. So one feels pressured into adding code
// to free the global data.
//
// This doesn't sound hard (and indeed in this simple example it would not be hard to free
// the global data), but sometimes that data resides in many data structures across many
// subsystems of a large program, and freeing that stuff adds nontrivial complexity.
// We take an alternate approach: you can call a procedure that tells the leak detector
// that this memory should not be counted as a leak; instead it's intentional global data.
// The nice thing is, this can be done at the site of the allocation, rather than having
// code spread out in two places that have to coordinate with each other logically (which
// is one of the things that makes programs complicated).
the_status := New(Status); // Allocate a global Status.
this_allocation_is_not_a_leak(the_status); // Tell the memory debugger it's not a leak.
// (We can call this_allocation_is_not_a_leak even if memory debugging is disabled;
// Basic defines it to be an empty macro if MEMORY_DEBUGGING==false.)
// Now let's do some main program stuff:
numbers: [] int;
for i: 1..10 {
numbers = make_numbers(i); // make_numbers gives us a heap-allocated array.
sum := 0;
for numbers sum += it;
print("Sum is: %.\n", sum);
// Oops... we forgot to free 'numbers'!
}
// Let's start allocating using a Pool...
pool: Pool;
set_allocators(*pool);
fruit_copies: [fruits.count] string;
{
// Allocate a copy of each fruit from the Pool.
// This will show up in the report as just one allocation,
// performed by the Pool, of the page onto which all these fruits fit.
push_allocator(pool_allocator_proc, *pool);
for fruits fruit_copies[it_index] = copy_string(it);
}
// We are back using the default allocator now.
// What happens if we try to free memory, allocated from Pool,
// with the default allocator (this is wrong!!)
// Uncomment this to see...
// free(fruit_copies[0]);
// At the end of execution, or any time really, we can report memory leaks.
// Here's the simplest way to do it:
print("\n\n **** First Leak Report: ****\n\n");
report_memory_leaks();
//
// We're going to do another leak report. But before we do that, let's free
// whichever was the last 'numbers' array we allocated, to show that the report
// notices we freed stuff.
//
free(numbers.data);
//
// For a big program, these leak reports can become verbose, and this can make it harder
// to understand what is a legitimate leak. But we can easily trim the reports down,
// because there are many procedures that we just know allocate memory, so if we called
// one of those, we don't need to see further down the call stack. For example,
// if we call copy_string, it's obvious that this allocates memory, so we don't need
// to show that this then calls alloc_string, which then calls alloc. Trimming this
// for every report can make things substantially more readable. We indicate what to trim
// by setting known_allocation_leaves. It's a structure with two strings; the first is
// the name of the procedure, and the second is a substring of the module or file that
// it lives in. (Use the empty string to skip this substring match and just match on
// the name of the procedure).
//
{
print("\n\n **** Second Leak Report, without common leaves: ****\n\n");
options: Leak_Report_Options;
options.known_allocation_leaves = .[.{"New", "Basic"}, .{"NewArray", "Basic"}, .{"array_add", "Basic"}, .{"array_resize", "Basic"}, .{"array_reserve", "Basic"}, .{"copy_string", "Basic"}, .{"alloc_string", "Basic"}, .{"add_bucket", "Bucket_Array"}, .{"find_and_occupy_empty_slot", "Bucket_Array"}, .{"init", "Hash_Table"}, .{"resize", "Hash_Table"}, .{"table_add", "Hash_Table"}, .{"read_entire_file", "File"}, .{"get", "Pool"}];
report_memory_leaks(options);
}
//
// In a larger program, you may get several levels down in the call stack before
// your code starts to go anywhere interesting -- say from main, to a procedure that
// runs the main loop, to a procedure that does all the simulation. Seeing these top
// few procedures all the time in every report may make it verbose and increase the
// noise level, so you can trim things out of the *bottom* of the call stack by
// setting roots_to_skip. This works differently than known_allocation_leaves:
// here, we start at the root and trim only so long as the current function is
// inside 'roots_to_skip'. As soon as we are outisde that set, we stop trimming.
//
{
print("\n\n **** Third Leak Report, without main: ****\n\n");
options: Leak_Report_Options;
options.known_allocation_leaves = .[.{"New", "Basic"}, .{"NewArray", "Basic"}, .{"array_add", "Basic"}, .{"copy_string", "Basic"}, .{"alloc_string", "Basic"}];
options.roots_to_skip = .[.{"main", ""}, .{"some_other_common_routine", ""}];
report_memory_leaks(options);
}
//
// As programs get bigger, sometimes these memory reports can get very noisy
// in other ways. For example, if you have a recursive procedure, or group of procedures
// that call each other, and then there is a function at the bottom of all this recursion
// that allocates, you might get to that allocation via many different call stacks.
// The system tells you all those different stacks as separate reports, because it
// doesn't know what information in those stacks is important to you and what isn't.
// But if you decide that differences between call stacks are irrelevant, you can
// give the reporter procedures such that all calls to that procedure go into the
// same group, and get reported once. Only the portions of the call stack that are
// common to all members of the group are shown.
//
//
// To illustrate this, let's add some more leaks that happen via a recursive function,
// then just look at the default report that is generated:
//
// (In order to make mutually recursive functions in an imperative scope, we need to do
// this hack where we use a struct, so that they live in a data scope... otherwise
// there's no way for them to call each other without one calling ahead into the other. Sigh!)
Holder :: struct {
handle_even_number :: (x: int) -> *void {
return recursive(x/2);
}
recursive :: (x: int) -> *void {
if x <= 1 return alloc(100);
if (x % 5) == 0 return recursive(x/5);
if (x & 1) return recursive(x-1);
return handle_even_number(x);
}
}
recursive :: Holder.recursive;
recursive(1);
recursive(5);
recursive(10);
recursive(11);
{
print("\n\n **** Fourth Leak Report, with extra noisy recursive stacks: ****\n\n");
options: Leak_Report_Options;
options.known_allocation_leaves = .[.{"copy_string", "Basic"}, .{"NewArray", "Basic"}, .{"get", "Pool"}];
report_memory_leaks(options);
}
//
// To make that report less noisy, we can just say, hey, anything that ever called 'recursive'
// goes into the same group.
//
{
print("\n\n **** Fifth Leak Report, with grouped stacks: ****\n\n");
options: Leak_Report_Options;
options.known_allocation_leaves = .[.{"copy_string", "Basic"}, .{"NewArray", "Basic"}, .{"get", "Pool"}];
options.group_by_these_calls = .[.{"recursive", ""}];
report_memory_leaks(options);
}
//
// You can also get a leak report as a data structure, rather than logging it as a string.
// So you can do whatever you want with it -- filter it in some way, draw it graphically,
// send it to a server, etc. And you don't have to deal with the slowness, errors and
// uncertainty involved in parsing string output.
//
print("\n\n **** As a Data Structure: ****\n\n");
report := make_leak_report();
print("%\n", report);
for report.sorted_summaries print("** Summary %: **\n%\n", it_index, it.*);
caps, name := get_capabilities(context.default_allocator);
print("Allocator is \"%\", caps: %\n", name, caps);
if caps & .CREATE_HEAP {
//
// Let's test first-class heaps. We should be able to create a heap,
// allocate individual things in it, then destroy the heap, and all
// individual allocations inside that heap should be known to be gone,
// without us having to free them individually. In other words, the total
// number of leaked allocations from our previous report should be equal
// to the total number of bytes and leaked allocations after we destroy
// that heap; we'll assert this.
//
leaked_count_before := 0;
leaked_bytes_before := 0;
for report.sorted_summaries {
leaked_count_before += it.count;
leaked_bytes_before += it.bytes;
}
deinit(*report);
// Let's make that messy first-class heap and do some stuff to it:
old_allocator := context.allocator;
new_heap := create_heap(context.allocator);
for 1..10 {
waste := alloc(69105,, allocator=new_heap);
}
// Let's count how much leakage we have before destroying the heap:
middle := make_leak_report();
leaked_count_middle := 0;
leaked_bytes_middle := 0;
for middle.sorted_summaries {
leaked_count_middle += it.count;
leaked_bytes_middle += it.bytes;
}
deinit(*middle);
// Now let's destroy that heap:
destroy_heap(new_heap);
// Now let's make a new leak report and make sure there are no more leaks:
print("\n\nAsserting that destruction of a messy first-class heap did not increase our leaks:\n");
after := make_leak_report();
leaked_count_after := 0;
leaked_bytes_after := 0;
for after.sorted_summaries {
leaked_count_after += it.count;
leaked_bytes_after += it.bytes;
}
deinit(*after);
print("leaked_count_before = %\n", leaked_count_before);
print("leaked_count_middle = %\n", leaked_count_middle);
print("leaked_count_after = %\n", leaked_count_after);
assert(leaked_count_before == leaked_count_after);
print("leaked_bytes_before = %\n", leaked_bytes_before);
print("leaked_bytes_middle = %\n", leaked_bytes_middle);
print("leaked_bytes_after = %\n", leaked_bytes_after);
assert(leaked_bytes_before == leaked_bytes_after);
} else {
print("Skipping the first-class heap test until we have an allocator that supports this.\n");
deinit(*report);
}
print("We're good! Done.\n");
}
make_numbers :: (n: int) -> [] int {
// Allocate, on the heap (or whatever allocator is currently set), an array of n integers, and return it.
result := NewArray(n, int);
for 0..n-1 result[it] = it;
return result;
}

View File

@ -0,0 +1,189 @@
main :: () {
now := current_time_consensus();
print("Now in Apollo Time: %\n", now);
calendar_utc := to_calendar(now, .UTC);
print("Calendar UTC in data structure form: %\n", calendar_utc);
print("\n");
s := calendar_to_string(calendar_utc);
print("Date in UTC is: %\n", s);
calendar_local := to_calendar(now, .LOCAL);
s = calendar_to_string(calendar_local);
print("Date in Local is: %\n", s);
print("\n");
#if OS != .WINDOWS {
print("Unfortunately, since we currently use OS-specific routines to convert to calendar time,\n");
print("and Linux's time structs do not go far enough into the past, this next call will report an error,\n");
print("and this is a travesty. We'll fix this before too long.\n");
}
print("Apollo T plus zero in UTC is %\n", calendar_to_string(to_calendar(.{}, .UTC)));
print("\n");
//
// Let's look at the total range of Apollo_Time, in years.
//
// The maximum positive Apollo_Time is just the normal two's complement thing
// of all bits being 1, except the high bit:
apollo_max: Apollo_Time;
apollo_max.high = 0x7fff_ffff_ffff_ffff; // Maximum positive value.
apollo_max.low = 0xffff_ffff_ffff_ffff;
seconds_per_day :: 60*60*24;
seconds_per_year :: seconds_per_day*365 + seconds_per_day/4;
// Here we are using an overloaded operator / to divide an Apollo_Time by a constant:
apollo_seconds := apollo_max / 1_000_000_000_000_000;
print("Apollo max seconds (rounded down): %\n", apollo_seconds);
apollo_years := apollo_seconds / seconds_per_year;
assert(apollo_years.high == 0); // 10**15 * seconds_per_year is a little more than 64 bits, so the result of the divide is less than 64 bits.
years := apollo_years.low;
print("The maximum representable Apollo_Time is % years in the future (rounded down).\n",
formatInt(years, comma_string=",", digits_per_comma=3));
print("\n");
//
// If the divide operation worked properly, then the remainder of the divide
// should be less than seconds_per_year. Let's check that.
//
// Here we will build a femtoseconds_per_year number. seconds_per_year fits in 64 bits, and
// FEMTOSECONDS does too (it is the constant value 10**15), but the product does not. So we
// put one of the terms in the bottom words of an Apollo_Time, then use the overloaded *
// to multiply in the second term.
femtoseconds_per_year: Apollo_Time;
femtoseconds_per_year.low = FEMTOSECONDS;
femtoseconds_per_year *= seconds_per_year;
// Then we multiply this by our number of years, to get the integer number of years
// that fits into Apollo_Time; this will be somewhat close to apollo_max, but not quite there.
back := femtoseconds_per_year * cast(s64) years; // This operator is only overloaded for s64 right now.
FormatApollo :: (a: Apollo_Time) -> string {
Format :: (x: Any) -> FormatInt {
return formatInt(x, base=16, digits_per_comma=4, comma_string="_", minimum_digits=16);
}
// We are writing it as high then low, as a long hex number.
// This is different from what you see when you just print the structure,
// which prints low then high, because low is first in memory.
return tprint("0x% %", Format(a.high), Format(a.low));
}
print("The max Apollo_Time, in hex, is ............................. : %\n", FormatApollo(apollo_max));
print("When we multiply femtoseconds_per_year by apollo_years, we get: %\n", FormatApollo(back));
delta := apollo_max - back;
print("Subtracting these, we get ................................... : %\n", FormatApollo(delta));
print("This must be less than femtoseconds_per_year, which is ...... : %\n", FormatApollo(femtoseconds_per_year));
assert(delta < femtoseconds_per_year);
print("\n");
{
// Show use of monotonic time, plus conversion to float and back.
// (You might convert a per-frame dt to float in order to easily do math,
// because you know the dt is pretty small relative to the age of the Universe.)
t0 := current_time_monotonic();
sleep_milliseconds(100);
t1 := current_time_monotonic();
dt := t1 - t0;
dt_float := to_float64_seconds(dt);
print("dt is % seconds (Apollo format).\n", dt);
print("dt is % seconds (float64).\n", dt_float);
// Convert from float back to Apollo. We can see how much
// precision we lost. For monotonic timers that have a small
// output range, the answer is possibly none!
dt_backconverted := to_apollo(dt_float);
print("dt_backconverted %\n", dt_backconverted);
print("Difference induced by conversion to float and back: %\n", dt - dt_backconverted);
}
//
// Let's just do a few random multiplies and divides to test operators:
//
delta_t := Apollo_Time.{336, 669};
divided3 := delta_t / 3;
multiplied3 := divided3 * 3;
assert(multiplied3 == delta_t); // Because the original was a multiple of 3.
divided4 := delta_t / 4;
multiplied4 := divided4 * 4;
assert(multiplied4 == delta_t); // It was also a multiple of 4!
print("delta_t %\n", delta_t);
print("delta_t %\n", FormatApollo(delta_t));
print("\n");
print("divided3 %\n", divided3);
print("multiplied3 %\n", multiplied3);
print("\n");
print("divided4 %\n", divided4);
print("multiplied4 %\n", multiplied4);
print("\n");
// Bit shift operations:
shift_right_2 := delta_t >> 2;
print("shift_right_2 (should be equal to divided4) %\n", shift_right_2);
assert(shift_right_2 == divided4);
shift_left_then_right_2 := (delta_t << 2) >> 2;
print("shift_left_then_right_2 (should be equal to delta_t) %\n", shift_left_then_right_2);
assert(shift_left_then_right_2 == delta_t);
//
// Multiply and divide by Apollo_Time:
//
factor := Apollo_Time.{100, 0};
// Multiply both ways, just to see:
product1 := delta_t * factor;
print("product1 %\n", product1);
product2 := factor * delta_t;
print("product2 %\n", product2);
assert(product1 == product2);
print("\n");
//
// Simple dumb divide test, dividing by 1:
//
print("Divided by 1: %\n", apollo_max/1);
assert(apollo_max/1 == apollo_max);
}
#import "Basic";

View File

@ -0,0 +1,244 @@
FIXED_ARENA_DEFAULT_ALIGNMENT :: 16;
FIXED_ARENA_HEADER_BYTES :: size_of(FixedArena);
FixedArena :: struct {
memory: []u8;
cursor: s64;
allocator: Allocator;
}
fixed_arena_alloc :: (arena: *FixedArena, byte_count: s64) -> *void {
if !is_valid(arena) {
log_error("[Basic/FixedArena] Attempting to allocate from an invalid or null arena: %!\n", arena);
return null;
}
if byte_count < 0 {
log_error("[Basic/FixedArena] Arena %: negative allocation size %.\n", arena, byte_count);
return null;
}
// returned pointer is deliberately unaligned for size 0
if byte_count == 0 return arena.memory.data + arena.cursor;
capacity := arena.memory.count;
result, aligned := fixed_arena_next_start(arena);
if aligned > capacity || byte_count > capacity - aligned {
log_error("[Basic/FixedArena] Attempt to allocate, but Arena % is full! (Allocation size %; alignment %; capacity %; already allocated %.)\n",
arena, byte_count, FIXED_ARENA_DEFAULT_ALIGNMENT, capacity, arena.cursor);
return null;
}
arena.cursor = aligned + byte_count;
return result;
}
// Resizes a block, extending it where it sits when it is the most recent allocation.
// Array growth is the dominant caller of RESIZE, so that is the common path
fixed_arena_resize :: (arena: *FixedArena, old_memory: *void, old_size: s64, requested_size: s64) -> *void {
if !is_valid(arena) {
log_error("[Basic/FixedArena] Attempting to resize against an invalid or null arena: %!\n", arena);
return null;
}
if requested_size < 0 {
log_error("[Basic/FixedArena] Arena %: negative resize size %.\n", arena, requested_size);
return null;
}
if old_memory && old_size > 0 {
old_start := cast(*u8)old_memory;
old_end := old_start + old_size;
// Check that the last allocation is the same as the one we want to resize:
if old_start >= fixed_arena_start(arena) && old_end == arena.memory.data + arena.cursor {
old_offset := cast(s64)(old_start - arena.memory.data);
if requested_size <= arena.memory.count - old_offset {
arena.cursor = old_offset + requested_size; // shrinking reclaims, growing extends
return old_memory;
}
}
}
// Fallback:
result := fixed_arena_alloc(arena, requested_size);
// A resize may legitimately arrive with a old_memory that is null
size_to_copy := min(old_size, requested_size);
if result && old_memory && size_to_copy > 0 {
memcpy(result, old_memory, size_to_copy);
}
return result;
}
fixed_arena_allocator_proc :: (mode: Allocator_Mode, requested_size: s64, old_size: s64, old_memory: *void, allocator_data: *void) -> *void {
arena: *FixedArena = xx allocator_data;
if #complete mode == {
case .FREE;
return null;
case .RESIZE;
return fixed_arena_resize(arena, old_memory, old_size, requested_size);
case .ALLOCATE;
return fixed_arena_alloc(arena, requested_size);
case .STARTUP; #through;
case .SHUTDOWN;
return null;
case .THREAD_START; #through;
case .THREAD_STOP;
return null;
case .CREATE_HEAP; #through;
case .DESTROY_HEAP;
return null;
case .IS_THIS_YOURS;
if !is_valid(arena) return null;
if old_memory >= fixed_arena_start(arena) && old_memory < arena.memory.data + arena.cursor {
return cast(*void)true;
}
return null;
case .CAPS;
if old_memory { (cast(*string)old_memory).* = "modules/Basic fixed_arena_allocator_proc"; }
return cast(*void)(Allocator_Caps.IS_THIS_YOURS | .HINT_I_AM_A_FAST_BUMP_ALLOCATOR);
}
}
FIXED_ARENA_MINIMUM_SIZE :: size_of(FixedArena) + FIXED_ARENA_DEFAULT_ALIGNMENT;
fixed_arena_init :: (arena: *FixedArena, memory: []u8, allocator := Allocator.{}) -> bool {
if !arena {
log_error("[Basic/FixedArena] Attempting to initialize a null arena!\n");
return false;
}
if memory.data == null || memory.count < FIXED_ARENA_MINIMUM_SIZE {
log_error("[Basic/FixedArena] Cannot initialize an arena over a % byte block; the minimum is %.\n",
memory.count, FIXED_ARENA_MINIMUM_SIZE);
return false;
}
arena.memory = memory;
arena.cursor = FIXED_ARENA_HEADER_BYTES;
arena.allocator = allocator;
return true;
}
bootstrap_fixed_arena :: (size: s64) -> *FixedArena {
if size < FIXED_ARENA_MINIMUM_SIZE {
log_error("[Basic/FixedArena] Requested size % is below the minimum of % bytes (a % byte header plus one % byte aligned allocation).\n",
size, FIXED_ARENA_MINIMUM_SIZE, size_of(FixedArena), FIXED_ARENA_DEFAULT_ALIGNMENT);
return null;
}
memory := NewArray(size, u8, false);
if memory.data == null {
log_error("[Basic/FixedArena] Failed to allocate % bytes for a new arena.\n", size);
return null;
}
result := cast(*FixedArena)memory.data;
result.memory = memory;
result.cursor = FIXED_ARENA_HEADER_BYTES;
result.allocator = context.allocator;
return result;
}
delete_fixed_arena :: (arena: *FixedArena) {
if !arena return;
memory := arena.memory;
allocator := arena.allocator;
if memory.data == null return;
if allocator.proc == null return;
free(memory.data,, allocator);
}
get_allocator :: (arena: *FixedArena) -> Allocator #expand {
return .{fixed_arena_allocator_proc, arena};
}
fixed_arena_start :: (arena: *FixedArena) -> *u8 #expand {
return arena.memory.data + FIXED_ARENA_HEADER_BYTES;
}
is_valid :: (arena: *FixedArena) -> bool {
if !arena return false;
return arena.memory.data != null
&& arena.memory.count >= FIXED_ARENA_HEADER_BYTES
&& arena.cursor >= 0
&& arena.cursor <= arena.memory.count;
}
fixed_arena_reset :: (arena: *FixedArena) {
if !is_valid(arena) return;
arena.cursor = FIXED_ARENA_HEADER_BYTES;
}
push_arena :: (arena: *FixedArena) #expand #no_debug {
assert(is_valid(arena));
old_allocator := context.allocator;
context.allocator = get_allocator(arena);
`defer context.allocator = old_allocator;
}
auto_reset :: (arena: *FixedArena) #expand #no_debug {
__mark := get_fixed_arena_state(arena);
`defer restore_fixed_arena_state(arena, __mark);
}
// The largest byte_count the next fixed_arena_alloc can satisfy.
fixed_arena_available_bytes :: (arena: *FixedArena) -> s64 {
if !is_valid(arena) return 0;
start, offset := fixed_arena_next_start(arena);
if offset >= arena.memory.count return 0;
return arena.memory.count - offset;
}
#scope_module
// Where the next aligned allocation would begin
fixed_arena_next_start :: (arena: *FixedArena) -> (start: *u8, offset: s64) #expand {
base := arena.memory.data;
start := Align(base + arena.cursor, FIXED_ARENA_DEFAULT_ALIGNMENT);
return start, cast(s64)(start - base);
}
Fixed_Arena_State :: struct {
starting_arena : *FixedArena;
starting_point : s64;
}
get_fixed_arena_state :: inline (arena: *FixedArena) -> Fixed_Arena_State {
assert(arena != null);
state: Fixed_Arena_State = ---;
state.starting_arena = arena;
state.starting_point = arena.cursor;
return state;
}
restore_fixed_arena_state :: inline (arena: *FixedArena, state: Fixed_Arena_State) {
assert(arena == state.starting_arena);
assert(is_valid(state.starting_arena));
state.starting_arena.cursor = state.starting_point;
}

View File

@ -0,0 +1,728 @@
// This file contains a port of the floating-point printing functionality in stb_sprintf; the original code has been
// significantly refactored to support this single use case.
stb_print_float :: (builder: *String_Builder, format: FormatFloat) -> int {
copy_repeated_character :: (destination: *u8, count: int, $character: u8) {
c :: cast,no_check(u32) character;
characters_wide : u32 : (c << 24) | (c << 16) | (c << 8) | c;
i := count;
d := destination;
while i {
if ((cast,no_check(u64) d) & 3) == 0 break;
d.* = character;
d += 1;
i -= 1;
}
while i >= 4 {
(cast(*u32) d).* = characters_wide;
d += 4;
i -= 4;
}
while i {
d.* = character;
d += 1;
i -= 1;
}
}
copy_integer_part :: (count: int, s: *u8) -> (n: int, s: *u8) #expand {
// This is just a macro so we can use these variables without passing them...
n := 0;
if use_thousand_separators {
while n < count {
cs += 1;
if cs > thousand_group_size {
cs = 0;
s.* = thousand_separator_character;
s += 1;
} else {
s.* = (sn + n).*;
s += 1;
n += 1;
}
}
} else {
while n < count {
s.* = (sn + n).*;
s += 1;
n += 1;
}
}
return n, s;
}
determine_mode_for_shortest :: () -> FormatFloat.Mode #expand {
// clamp the precision and delete extra zeros after clamp
n = pr;
if l > pr l = pr;
while l > 1 && pr && sn[l - 1] == #char "0" {
pr -= 1;
l -= 1;
}
if dp <= -4 || dp > n {
if pr > l pr = l - 1;
else if pr pr -= 1; // when using %e, there is one digit before the decimal
return .SCIENTIFIC;
} else {
// this is the insane action to get the pr to match %g sematics for %f
if dp > 0 pr = ifx dp < l then l - dp else 0;
else pr = -dp + ifx pr > l then l else pr;
return .DECIMAL;
}
}
num: [512] u8; // Big enough for e308 (with thousand separators) or e-307.
exponent: [8] u8; // Stores the exponent string at index 1 and the length of the string at index 0.
fw, pr, tz, dp, l, n, cs: int;
s, sn: *u8;
total_string_length: int;
trailing_zeroes_in_string: int;
decimal_digits_in_string: int;
// @Cleanup: We can just 'using' format here:
decimal_separator_character := format.decimal_separator_character;
thousand_separator_character := format.thousand_separator_character;
exponent_character := format.exponent_character;
use_thousand_separators := format.thousand_separators;
mode_to_use := format.mode;
// @Note: These features aren't available through FormatFloat yet.
use_left_justification :: false;
left_pad_with_zeroes :: false;
fw = format.width;
pr = format.trailing_width;
if pr < 0 pr = default_precision;
any := format.value;
assert(any.type != null);
type := any.type;
while type.type == .VARIANT {
variant := cast(*Type_Info_Variant) type;
type = variant.variant_of;
}
assert(type.type == .FLOAT);
info := cast(*Type_Info_Float) type;
float_value: float64;
if info.runtime_size == 8 {
float_value = (.*)cast(*float64) format.value.value_pointer;
} else {
float_value = (.*)cast(*float32) format.value.value_pointer;
}
pr_flag, pr_bias: u32;
if mode_to_use == {
case .SHORTEST;
if pr == 0 pr = 1;
pr_bias = 1;
pr_flag = 0x80000000;
case .SCIENTIFIC; pr_flag = 0x80000000;
}
is_negative := real_to_str(*sn, *l, num.data, *dp, float_value, (cast,no_check(u32)pr - pr_bias) | pr_flag);
prefix_character: u8;
if is_negative {
prefix_character = #char "-";
} else if format.positive_number_prefix == {
case .SPACE; prefix_character = #char " ";
case .PLUS; prefix_character = #char "+";
}
if dp == INFINITY_OR_NAN {
s = sn; cs = 0; pr = 0;
} else {
if mode_to_use == .SHORTEST mode_to_use = determine_mode_for_shortest();
if mode_to_use == .DECIMAL {
//
// Decimal float.
//
s_start := num.data + 64;
s = s_start;
// handle the three decimal varieties
if dp <= 0 { // handle 0.000*000xxxx
s.* = #char "0"; s += 1;
if pr { s.* = decimal_separator_character; s += 1; }
first_trailing_zero_in_string := s;
n = -dp;
if n > pr n = pr;
copy_repeated_character(s, n, #char "0"); s += n;
if l + n > pr l = pr - n;
decimal_digits_in_string = n + l;
for 1..l {
s.* = sn.*;
if sn.* != #char "0" first_trailing_zero_in_string = s + 1;
s += 1; sn += 1;
}
tz = pr - (n + l);
cs = 1 + (thousand_group_size << 24); // how many tens did we write (for commas below)
trailing_zeroes_in_string = s - first_trailing_zero_in_string;
} else {
cs = ifx use_thousand_separators then (600 - dp) % thousand_group_size else 0;
if dp >= l { // handle xxxx000*000.0
n, s = inline copy_integer_part(l, s);
if n < dp { // Copy the remaining zeroes of the integer part.
n = dp - n;
if !use_thousand_separators {
copy_repeated_character(s, n, #char "0"); s += n;
} else {
while n {
cs += 1;
if cs > thousand_group_size {
cs = 0;
s.* = thousand_separator_character; s += 1;
} else {
s.* = #char "0"; s += 1;
n -= 1;
}
}
}
}
cs = (s - s_start) + (thousand_group_size << 24); // cs is how many tens
if pr {
s.* = decimal_separator_character; s += 1;
tz = pr;
}
} else { // handle xxxxx.xxxx000*000
n, s = inline copy_integer_part(dp, s);
cs = (s - s_start) + (thousand_group_size << 24); // cs is how many tens
if pr { s.* = decimal_separator_character; s += 1; }
if l - dp > pr l = pr + dp;
decimal_digits_in_string = l - n;
first_trailing_zero_in_string := s;
sn = sn + n;
n = l - n;
for 1..n {
s.* = sn.*;
if sn.* != #char "0" first_trailing_zero_in_string = s + 1;
s += 1; sn += 1;
}
tz = pr - (l - dp);
trailing_zeroes_in_string = s - first_trailing_zero_in_string;
}
}
pr = 0;
l = s - s_start; // get the length that we copied
s = s_start;
} else {
//
// Scientific float.
//
s_start := num.data + 64;
s = s_start;
// handle leading chars
s.* = sn.*; s += 1;
if pr { s.* = decimal_separator_character; s += 1; }
// handle after decimal
if (l - 1) > pr l = pr + 1;
decimal_digits_in_string = l - 1;
first_trailing_zero_in_string := s;
n = 1;
while n < l {
s.* = (sn + n).*;
if s.* != #char "0" first_trailing_zero_in_string = s + 1;
s += 1; n += 1;
}
trailing_zeroes_in_string = s - first_trailing_zero_in_string;
// trailing zeros
tz = pr - (l - 1);
pr = 0;
// dump expo
exponent[1] = exponent_character;
dp -= 1;
if dp < 0 {
exponent[2] = #char "-";
dp = -dp;
} else {
exponent[2] = #char "+";
}
n = ifx dp >= 100 then 5 else 4;
exponent[0] = cast,no_check(u8)n;
while true {
exponent[n] = cast,no_check(u8)(#char "0" + (dp % 10));
if n <= 3 break;
n -= 1;
dp /= 10;
}
cs = 1 + (thousand_group_size << 24); // how many tens
l = s - s_start; // get the length that we copied
s = s_start;
}
}
//
// This used to be finalize_string():
//
append_repeated_character :: (builder: *String_Builder, count: int, $character: u8) -> int {
result := count;
while count {
available_range := ensure_contiguous_space_and_return_available_range(builder);
clamped_count := min(count, available_range.count);
count -= clamped_count;
copy_repeated_character(available_range.data, clamped_count, character);
advance_through_ensured_space(builder, clamped_count);
}
return result;
}
characters_removed_by_zero_removal: int;
if format.zero_removal != .NO && mode_to_use != .SHORTEST {
//
// :ZeroRemoval
//
// When we remove zeroes (and possibly the decimal separator itself), special behavior occurs when the width
// and/or trailing width are explicit (i.e., non-negative). When a width is set and a number of characters
// are removed by zero removal, the same number of spaces will be removed from any potential left padding,
// so that the printed number does not shift to the right. When a trailing width is set, any removed
// characters at the end will be replaced by spaces to maintain the width of the string.
//
// Note that the .SHORTEST mode already does its own version of zero removal, so using that in combination
// with a zero removal mode other than .YES might lead to somewhat surprising results.
//
if (decimal_digits_in_string || tz) && dp != INFINITY_OR_NAN {
if (decimal_digits_in_string > 0 && trailing_zeroes_in_string == decimal_digits_in_string) || (decimal_digits_in_string == 0 && tz > 0) {
if format.zero_removal == .YES {
characters_removed_by_zero_removal = trailing_zeroes_in_string + tz + 1;
l -= trailing_zeroes_in_string + 1;
tz = 0;
} else if format.zero_removal == .ONE_ZERO_AFTER_DECIMAL {
if trailing_zeroes_in_string {
characters_removed_by_zero_removal = (trailing_zeroes_in_string - 1) + tz;
l -= (trailing_zeroes_in_string - 1);
tz = 0;
} else {
characters_removed_by_zero_removal = tz - 1;
tz = 1;
}
}
} else if trailing_zeroes_in_string {
characters_removed_by_zero_removal = trailing_zeroes_in_string + tz;
l -= trailing_zeroes_in_string;
tz = 0;
} else if tz {
characters_removed_by_zero_removal = tz;
tz = 0;
}
}
}
// Calculate fw (= leading/trailing space) and pr (= leading zeroes).
pr = max(pr, l);
n = pr + exponent[0] + tz;
if prefix_character n += 1;
fw = max(fw, n);
fw -= n;
pr -= l;
// Maintain number alignment by taking the number of characters removed by zero removal and removing that many spaces from the left padding. :ZeroRemoval
fw -= min(fw, characters_removed_by_zero_removal);
if !use_left_justification {
if left_pad_with_zeroes { // if leading zeros, everything is in pr
pr = ifx fw > pr then fw else pr;
fw = 0;
} else {
use_thousand_separators = false;
}
}
// copy the spaces and/or zeros
if fw + pr {
// copy leading spaces (or when doing %8.4d stuff)
if !use_left_justification total_string_length += append_repeated_character(builder, fw, #char " ");
// copy leader
if prefix_character { append(builder, prefix_character); total_string_length += 1; }
// copy leading zeros
c := cs >> 24;
cs &= 0xffffff;
cs = ifx use_thousand_separators then c - ((pr + cs) % (c + 1)) else 0;
if pr {
if !use_thousand_separators {
total_string_length += append_repeated_character(builder, pr, #char "0");
} else {
for 1..pr {
if cs == c {
cs = 0;
append(builder, thousand_separator_character); total_string_length += 1;
} else {
cs += 1;
append(builder, #char "0"); total_string_length += 1;
}
}
}
}
} else if prefix_character { append(builder, prefix_character); total_string_length += 1; }
append(builder, s, l); total_string_length += l; // Copy number string.
total_string_length += append_repeated_character(builder, tz, #char "0"); // Copy trailing zeros.
append(builder, *exponent[1], exponent[0]); total_string_length += exponent[0]; // Copy exponent (if there is one).
// Add spaces back to compensate for removed characters. :ZeroRemoval
if format.trailing_width > -1 total_string_length += append_repeated_character(builder, characters_removed_by_zero_removal, #char " ");
// In case of left justify, put the spaces at the end.
if use_left_justification && fw > 0 total_string_length += append_repeated_character(builder, fw, #char " ");
return total_string_length;
}
#scope_file
default_precision :: 6;
thousand_group_size :: 3;
nan_label :: "NaN";
inf_label :: "Inf";
INFINITY_OR_NAN :: 0x7000;
// =======================================================================
// low level float utility functions
COPYFP :: inline (dest: *void, src: *void) {
(.*)cast(*u64)dest = (.*)cast(*u64)src;
}
bot :: float64.[0h3ff0000000000000, 0h4024000000000000, 0h4059000000000000, 0h408f400000000000, 0h40c3880000000000, 0h40f86a0000000000, 0h412e848000000000, 0h416312d000000000, 0h4197d78400000000, 0h41cdcd6500000000, 0h4202a05f20000000, 0h42374876e8000000, 0h426d1a94a2000000, 0h42a2309ce5400000, 0h42d6bcc41e900000, 0h430c6bf526340000, 0h4341c37937e08000, 0h4376345785d8a000, 0h43abc16d674ec800, 0h43e158e460913d00, 0h4415af1d78b58c40, 0h444b1ae4d6e2ef50, 0h4480f0cf064dd592];
negbot :: float64.[0h3fb999999999999a, 0h3f847ae147ae147b, 0h3f50624dd2f1a9fc, 0h3f1a36e2eb1c432d, 0h3ee4f8b588e368f1, 0h3eb0c6f7a0b5ed8d, 0h3e7ad7f29abcaf48, 0h3e45798ee2308c3a, 0h3e112e0be826d695, 0h3ddb7cdfd9d7bdbb, 0h3da5fd7fe1796495, 0h3d719799812dea11, 0h3d3c25c268497682, 0h3d06849b86a12b9b, 0h3cd203af9ee75616, 0h3c9cd2b297d889bc, 0h3c670ef54646d497, 0h3c32725dd1d243ac, 0h3bfd83c94fb6d2ac, 0h3bc79ca10c924223, 0h3b92e3b40a0e9b4f, 0h3b5e392010175ee6];
negboterr :: float64.[0hbc5999999999999a, 0hbc0eb851eb851eb8, 0hbbd89374bc6a7efa, 0hbbb6a161e4f765fe, 0hbb8ee78183f91e64, 0h3b4b5a63f9a49c2c, 0h3b15e1e99483b023, 0hbad03023df2d4c94, 0hbab34674bfabb83b, 0hba720a5465df8d2c, 0h3a47f7bc7b4d28aa, 0h39f97f27f0f6e886, 0hb9cecd79a5a0df95, 0h394ea70909833de7, 0hb97937831647f5a0, 0h3925b4c2ebe68799, 0hb90db7b2080a3029, 0hb8d7c628066e8cee, 0h388a52b31e9e3d07, 0h38675447a5d8e536, 0h383f769fb7e0b75e, 0hb7fa7566d9cba769];
top :: float64.[0h44b52d02c7e14af6, 0h497c06a5ec5433c6, 0h4e428bc8abe49f64, 0h53088ba3bf284e24, 0h57d03e29f5c2b18c, 0h5c957f48bb41db7c, 0h615c73892ecbfbf4, 0h6622d3d6f88f0b3d, 0h6ae8eb0138858d0a, 0h6fb07d457124123d, 0h7475d2ce55747a18, 0h793ce2137f743382, 0h7e031cfd3999f7b0];
negtop :: float64.[0h3b282db34012b251, 0h366244ce242c5561, 0h319b9b6364f30304, 0h2cd4dbf7b3f71cb7, 0h280f8587e7083e30, 0h2347d12a4670c123, 0h1e81fee341fc585d, 0h19bb31bb5dc320d2, 0h14f48c22ca71a1bd, 0h102f0ce4839198db, 0h0b677603725064a8, 0h06a1ba03f5b21000, 0h01dac9a7b3b7302f];
toperr :: float64.[0h4160000000000000, 0h45ebb542c80deb40, 0hcae83b80b9aab60a, 0hcfa32e22d17a166c, 0hd4523606902e180e, 0hd9296fb782462e87, 0hddf358952c0bd011, 0he2a78c1376a34b6c, 0he7817569fc243adf, 0hec5d9365a897aaa6, 0h7119050c256123a0, 0hf5db1799d76cc7a6, 0hfaa213fe39571a38];
negtoperr :: float64.[0h37c13badb829e079, 0hb2ee46a98d3d9f64, 0h2e3227c7218a2b65, 0h2951d96999aa01e9, 0ha4acc2229efc3962, 0h9fecd04a2263407a, 0h9b123b80f187a157, 0h965c4e22914ed912, 0h119bc296cdf42f82, 0h8cc9f9e7f4e16fe1, 0h880aeb0a72a8902a, 0h834e228e12c13408, 0h0000000000fa1259];
powten :: u64.[1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000, 100000000000, 1000000000000, 10000000000000, 100000000000000, 1000000000000000, 10000000000000000, 100000000000000000, 1000000000000000000, 10000000000000000000];
tento19th : u64 : 1000000000000000000;
ddmult_high :: (oh: float64, ol: float64, xh: float64, yh: float64) -> (oh: float64, ol: float64) {
ahi: float64 = 0;
bhi: float64 = 0;
bt: s64 = ---;
new_oh := xh*yh;
COPYFP(*bt, *xh);
bt &= cast,no_check(s64)((~cast,no_check(u64)0) << 27);
COPYFP(*ahi, *bt);
alo := xh - ahi;
COPYFP(*bt, *yh);
bt &= cast,no_check(s64)((~cast,no_check(u64)0) << 27);
COPYFP(*bhi, *bt);
blo := yh - bhi;
new_ol := ((ahi * bhi - new_oh) + ahi * blo + alo * bhi) + alo * blo;
return new_oh, new_ol;
}
ddtoS64 :: inline (xh: float64, xl: float64) -> s64 {
bits := cast,no_check(s64)xh;
vh := cast,no_check(float64) bits;
ahi := xh - vh;
t := ahi - xh;
alo := (xh - (ahi - t)) - (vh + t);
bits += cast,no_check(s64)(ahi + alo + xl);
return bits;
}
ddrenorm :: (oh: float64, ol: float64) -> (oh: float64, ol: float64) {
s := cast,no_check(float64)(oh + ol);
return s, ol - (s - oh);
}
ddmult_low :: inline (ol: float64, xh: float64, xl: float64, yh: float64, yl: float64) -> (ol: float64) {
return ol + xh*yl + xl*yh;
}
ddmult_low_small :: inline (ol: float64, xh: float64, yl: float64) -> (ol: float64) {
return ol + xh*yl;
}
raise_to_power10 :: (ohi: *float64, olo: *float64, d: float64, power: s32 /* power can be -323 to +350 */) {
ph, pl: float64;
if (power >= 0) && (power <= 22) {
ph, pl = ddmult_high(ph, pl, d, bot[power]);
} else {
e, et, eb: s32;
p2h, p2l: float64;
e = power;
if power < 0 e = -e;
et = (e * 0x2c9) >> 14;/* %23 */
if et > 13 et = 13;
eb = e - (et * 23);
ph = d; pl = 0;
if power < 0 {
if eb {
eb -= 1;
ph, pl = ddmult_high(ph, pl, d, negbot[eb]);
pl = ddmult_low_small(pl, d, negboterr[eb]);
}
if et {
ph, pl = ddrenorm(ph, pl);
et -= 1;
p2h, p2l = ddmult_high(p2h, p2l, ph, negtop[et]);
p2l = ddmult_low(p2l, ph, pl, negtop[et], negtoperr[et]);
ph = p2h;
pl = p2l;
}
} else {
if eb {
e = eb;
if eb > 22 eb = 22;
e -= eb;
ph, pl = ddmult_high(ph, pl, d, bot[eb]);
if e {
ph, pl = ddrenorm(ph, pl);
p2h, p2l = ddmult_high(p2h, p2l, ph, bot[e]);
p2l = ddmult_low_small(p2l, bot[e], pl);
ph = p2h;
pl = p2l;
}
}
if et {
ph, pl = ddrenorm(ph, pl);
et -= 1;
p2h, p2l = ddmult_high(p2h, p2l, ph, top[et]);
p2l = ddmult_low(p2l, ph, pl, top[et], toperr[et]);
ph = p2h;
pl = p2l;
}
}
}
ph, pl = ddrenorm(ph, pl);
ohi.* = ph;
olo.* = pl;
}
// given a float value, returns the significant bits in bits, and the position of the
// decimal point in decimal_pos. +/-INF and NAN are specified by special values
// returned in the decimal_pos parameter.
// frac_digits is absolute normally, but if you want from first significant digits (got %g and %e), or in 0x80000000
real_to_str :: (start: **u8, len: *int, out: *u8, decimal_pos: *int, value: float64, frac_digits: u32) -> bool {
bits: s64;
e, tens: s32;
d := value;
COPYFP(*bits, *d);
expo := cast,no_check(s32)((bits >> 52) & 2047);
is_negative := cast,no_check(bool)(bits >> 63);
if is_negative d = -d;
if expo == 2047 { // is nan or inf?
if bits & cast,no_check(s64)(((cast,no_check(u64)1) << 52) - 1) {
start.* = nan_label.data;
len.* = nan_label.count;
} else {
start.* = inf_label.data;
len.* = inf_label.count;
}
decimal_pos.* = INFINITY_OR_NAN;
return is_negative;
}
if expo == 0 { // is zero or denormal
if (bits << 1) == 0 { // do zero
decimal_pos.* = 1;
start.*= out;
out.* = #char "0";
len.* = 1;
return is_negative;
}
// find the right expo for denormals
{
one : u64 : 1;
v: s64 = cast,no_check(s64)(one << 51);
while (bits & v) == 0 {
expo -= 1;
v >>= 1;
}
}
}
// find the decimal exponent as well as the decimal bits of the value
{
ph, pl: float64;
// log10 estimate - very specifically tweaked to hit or undershoot by no more than 1 of log10 of all expos 1..2046
tens = expo - 1023;
tens = ifx tens < 0 then (tens * 617) / 2048 else ((tens * 1233) / 4096) + 1;
// move the significant bits into position and stick them into an int
raise_to_power10(*ph, *pl, d, 18 - tens);
// get full as much precision from double-double as possible
bits = ddtoS64(ph, pl);
// check if we undershot
if cast(u64)bits >= tento19th tens += 1;
}
// now do the rounding in integer land
#no_aoc { // In some (legit?) cases the tens + frac_digits can overflow.
frac_digits = ifx frac_digits & 0x80000000 then (frac_digits & 0x7ffffff) + 1 else cast,no_check(u32)tens + frac_digits;
}
if frac_digits < 24 {
dg: u32 = 1;
if cast,no_check(u64)bits >= powten[9] dg = 10;
done_fracing := false;
while cast,no_check(u64)bits >= powten[dg] {
dg += 1;
if dg == 20 {
done_fracing = true;
break;
}
}
if !done_fracing && frac_digits < dg {
// add 0.5 at the right position and round
e = cast,no_check(s32)dg - cast,no_check(s32)frac_digits;
if cast,no_check(u32)e < 24 {
r: u64 = powten[e];
bits = bits + cast,no_check(s64)(r / 2);
if cast,no_check(u64)bits >= powten[dg]
tens += 1;
bits /= cast,no_check(s64)r;
}
}
}
// kill long trailing runs of zeros
if bits {
done_killing := false;
while true {
if bits <= 0xffffffff break;
if bits % 1000 {
done_killing = true;
break;
}
bits /= 1000;
}
if !done_killing {
n := cast,no_check(u32)bits;
while (n % 1000) == 0
n /= 1000;
bits=n;
}
}
// convert to string
out += 64;
e = 0;
digitpair :: "00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899";
while true {
n: u32 = ---;
o := out - 8;
// do the conversion in chunks of U32s (avoid most 64-bit divides, worth it, constant denomiators be damned)
if bits >= 100000000 {
n = cast,no_check(u32)(bits % 100000000);
bits /= 100000000;
} else {
n = cast,no_check(u32)bits;
bits = 0;
}
while n {
out -= 2;
(.*)cast,no_check(*u16)out = (.*)cast,no_check(*u16)*digitpair[(n % 100) * 2];
n /= 100;
e += 2;
}
if bits == 0 {
if e && out.* == #char "0" {
out += 1;
e -= 1;
}
break;
}
while out != o {
out -= 1;
out.* = #char "0";
e += 1;
}
}
decimal_pos.* = tens;
start.*= out;
len.* = e;
return is_negative;
}

28
modules/Basic/linux.jai Normal file
View File

@ -0,0 +1,28 @@
// @Cleanup: Get rid of the importing of POSIX, as that is way too much.
#load "posix.jai";
get_working_directory :: () -> result: string { // Returns the empty string on failure.
PATH_MAX :: 4096;
buf: [PATH_MAX] u8;
result := getcwd(buf.data, buf.count);
// getcwd is supposed to malloc its return value if buf is null,
// but for some reason I was getting invalid free() on some versions
// of Ubuntu. Also it is probably better not to malloc. That said,
// we are adding this PATH_MAX limit, but, we don't know that such a
// limit does not exist internally to getcwd anyway. But, this is
// just kind of bleah.
/*
crt :: #library,system "libc";
c_free :: (memory: *void) #foreign crt "free";
defer c_free(cwd);
*/
if !result {
log_error("Unable to get the current directory: %.", errno());
return "";
}
return copy_temporary_string(to_string(buf.data));
}

719
modules/Basic/module.jai Normal file
View File

@ -0,0 +1,719 @@
// Simple procedures that many programs may want to use.
#load "Array.jai";
#load "Simple_String.jai";
#load "String_Builder.jai";
#load "Print.jai";
#load "Int128.jai";
#load "Apollo_Time.jai";
#load "string_to_float.jai";
#load "float_to_string.jai";
#load "fixed_arena.jai";
#load "serialization.jai";
#load "arena.jai";
#load "enums.jai";
#load "timing.jai";
#if (OS == .WINDOWS) || (OS == .PS5) || (OS == .NN_SWITCH2) || (OS == .LINUX) || (OS == .MACOS) {
#load "Temporary_Storage_Paged_Alloc.jai"; // @Incomplete: Flat_Pool version.
} else {
#load "Temporary_Storage_Paged_Alloc.jai";
}
// The suites for arena.jai and fixed_arena.jai live with the code they test so they can reach
// its module scope. They export run_arena_tests / run_fixed_arena_tests and nothing else;
// everything they use internally is #scope_file.
#if LOAD_TESTS {
#load "tests/arena_tests.jai";
#load "tests/fixed_arena_tests.jai";
}
#module_parameters () (MEMORY_DEBUGGER := false, ENABLE_ASSERT := true, REPLACEMENT_INTERFACE: $I/interface Memory_Debugger_Interface = Memory_Debugger_Interface, VISUALIZE_MEMORY_DEBUGGER := true, TEMP_ALLOCATOR_OVERWRITE_MEMORY := false, ARENA_DEBUG := false, LOAD_TESTS := false, ENABLE_TIMING := true) {
// Your custom-supplied memory debugger should conform to this interface:
Memory_Debugger_Interface :: struct {
check_alloc :: inline (allocator: Allocator, memory: *void, size: s64) {};
check_free :: inline (allocator: Allocator, memory: *void) {};
check_create_heap :: inline (a: Allocator) {};
check_destroy_heap :: inline (a: Allocator) {};
}
};
// secure_overwrite / memory_secure_overwrite used to live here. arena.jai still takes
// memory_secure_overwrite as its default $wipe_function. Only the main program may supply
// module parameters, so this import gets Secure_Memory at its defaults -- which is why its
// USE_SODIUM defaults to true rather than being something callers switch on.
#import "Secure_Memory";
// VISUALIZE_MEMORY_DEBUGGER and REPLACEMENT_INTERFACE only do anything if MEMORY_DEBUGGER == true.
// ENABLE_TIMING compiles in the tick-based timers in timing.jai.
// TEMP_ALLOCATOR_OVERWRITE_MEMORY memset free memory values to help track down use-after-free issues
SET_STORAGE_MARK_STAMP :: 0xda;
RESET_STORAGE_STAMP :: 0xdb;
FREE_PAGE_STAMP :: 0xdc;
//
// modules/Basic is intended to include a bunch of stuff that most programs will want to do;
// however, it is also supposed to be completely replaceable, so that you don't have to
// run on top of it if you don't want to.
//
// We could definitely use some comprehensive documentation on this module!
//
#if MEMORY_DEBUGGER {
#load "Memory_Debugger.jai";
#if VISUALIZE_MEMORY_DEBUGGER #load "Visualize_Memory_Debugger.jai";
#if REPLACEMENT_INTERFACE == Memory_Debugger_Interface { // Memory_Debugger_Interface is a sentinel; it means the user didn't pass a replacement, so if we see this, we just use our own stuff.
Memory_Debugger :: struct {
check_alloc :: _check_alloc;
check_free :: _check_free;
check_create_heap :: _check_create_heap;
check_destroy_heap :: _check_destroy_heap;
}
} else {
Memory_Debugger :: REPLACEMENT_INTERFACE;
}
} else {
// Declare this routine as a dummy so it can be used throughout the user-level code without #iffing it.
this_allocation_is_not_a_leak :: (memory: *void) #expand #no_debug {}
this_allocation_leaks_todo :: (memory: *void) #expand #no_debug {}
this_allocation_does_not_propagate_leak_ownership :: (memory: *void) #expand #no_debug {}
report_memory_leaks :: () {};
}
#if !(MEMORY_DEBUGGER && VISUALIZE_MEMORY_DEBUGGER) {
memory_visualizer_per_frame_update :: () #expand #no_debug {}
}
get_command_line_arguments :: () -> [] string {
c_args := __command_line_arguments;
result := NewArray(c_args.count, string);
for c_args result[it_index] = to_string(it);
return result;
}
assert_helper :: (message := "", args: .. Any, loc := #caller_location) -> bool {
if context.handling_assertion_failure return false; // Avoid infinite loops.
context.handling_assertion_failure = true;
// We provide 'loc' in case _STACK_TRACE is turned off, but assertion_failed may well
// look at the stack trace if it is available.
should_break := context.assertion_failed(loc, tprint(message, ..args));
context.handling_assertion_failure = false;
return should_break;
}
#if ENABLE_ASSERT {
// We make assert() a macro so that it doesn't annoy you by being on the call stack.
// To keep it short we factor most of the code into assert_helper() (which you could call
// from your own assert implementation, as it is included in the module even if ENABLE_ASSERT
// is false).
assert :: (arg: bool, message := "", args: .. Any, loc := #caller_location) #no_debug #expand {
if !arg && assert_helper(message, ..args, loc) then debug_break();
}
} else {
assert :: (#discard arg: bool, #discard message := "", #discard args: .. Any, #discard loc := #caller_location) #expand {}
// assert :: (arg: bool, message := "", args: .. Any, loc := #caller_location) #expand {}
}
alloc :: (size: s64) -> *void {
a := context.allocator;
result := a.proc(.ALLOCATE, size, 0, null, a.data);
#if MEMORY_DEBUGGER Memory_Debugger.check_alloc(a, result, size);
return result;
}
free :: (memory: *void) {
a := context.allocator;
#if MEMORY_DEBUGGER Memory_Debugger.check_free(a, memory);
a.proc(.FREE, 0, 0, memory, a.data);
}
realloc :: (memory: *void, size: s64, old_size: s64) -> *void {
a := context.allocator;
// We must not just do a "check_realloc" at the end (as previous versions of this function did)
// because that opens us up to race conditions where the memory debugger sees events in a different order that the allocator.
// If thread 1 reallocs some memory which frees address A and then thread 2 reallocs and gets address A
// the memory debugger will report incorrect double-alloc errors if it sees thread 2's realloc before the thread 1's free.
// This solution is not ideal because we will report free+alloc to the memory debugger even if the existing allocation
// was just resized in place.
// The other way to fix this would be to hold the memory debugger lock for the whole duration of the realloc
// to prevent the debugger from seeing events in a different order than the allocator. But this will probably slow down
// allocations too much in a contended multi-threaded scenario, so I opted for this solution.
// -rluba, 2026-06-19
#if MEMORY_DEBUGGER Memory_Debugger.check_free(a, memory);
result := a.proc(.RESIZE, size, old_size, memory, a.data);
#if MEMORY_DEBUGGER Memory_Debugger.check_alloc(a, result, size);
return result;
}
create_heap :: (a: Allocator) -> Allocator {
b_data := a.proc(.CREATE_HEAP, 0, 0, null, a.data);
b := Allocator.{a.proc, b_data};
#if MEMORY_DEBUGGER Memory_Debugger.check_create_heap(b);
return b;
}
destroy_heap :: (b: Allocator) {
#if MEMORY_DEBUGGER Memory_Debugger.check_destroy_heap(b);
b.proc(.DESTROY_HEAP, 0, 0, null, b.data);
}
get_capabilities :: (b: Allocator) -> (caps: Allocator_Caps, name: string) {
name: string;
result := cast,no_check(Allocator_Caps)b.proc(.CAPS, 0, 0, *name, null);
return result, name;
}
// Allocates a string.
alloc_string :: (count: int) -> string {
assert(count >= 0);
if !count return "";
s: string = ---;
s.data = alloc(count);
s.count = count;
return s;
}
talloc_string :: (count: int) -> string { // This is kind of silly now; we might deprecate it.
return inline alloc_string(count,, temp);
}
string_free :: inline (s: string) { free(s.data); }
free :: inline (s: string) { free(s.data); }
push_allocator :: (allocator: Allocator) #expand #no_debug { // This should probably be in caps or something, to tell people it's a macro.
old_allocator := context.allocator;
context.allocator = allocator;
`defer context.allocator = old_allocator;
}
push_allocator :: (proc: Allocator_Proc, data: *void) #expand #no_debug {
old_allocator := context.allocator;
context.allocator.proc = proc;
context.allocator.data = data;
`defer context.allocator = old_allocator;
}
// Should get_field not be in Basic? Do we use this any more?
get_field :: (info: *Type_Info_Struct, name: string) -> *Type_Info_Struct_Member, offset_in_bytes: s64 {
// First pass: Don't try recursing.
for * info.members {
if name == it.name return it, it.offset_in_bytes;
}
// Second pass: See if we can get something recursively.
for * info.members {
if (it.flags & .USING) && (it.type.type == .STRUCT) {
result, child_offset := get_field(cast(*Type_Info_Struct)it.type, name);
if result return result, it.offset_in_bytes + child_offset;
}
}
return null, 0;
}
min :: (a: $T, b: T) -> T { // Return the smallest of (a, b).
if a < b return a;
return b;
}
max :: (a: $T, b: T) -> T { // Return the largest of (a, b).
if b < a return a;
return b;
}
// In case you shadow 'min' and 'max' with local variables, provide capitalized versions...?
// I had originally made these capitalized until someone passive-aggressively changed them.
// Change them back? -jblow, 2 June 2021.
Min :: min;
Max :: max;
IsScalar :: (T: Type) -> bool #compile_time { // This is a utility used in the #modify routines for max, min, clamp.
ti := cast(*Type_Info) T;
if ti.type == {
// These are fine:
case .INTEGER; #through;
case .FLOAT; #through;
case .POINTER; #through;
case .ENUM;
return true;
}
return false;
}
max :: inline (a: $T, x: ..T) -> T
#modify { return IsScalar(T), "Argument is not of a scalar type."; }
{
m := a;
for x if m < it m = it;
return m;
}
min :: inline (a: $T, x: ..T) -> T
#modify { return IsScalar(T), "Argument is not of a scalar type."; }
{
m := a;
for x if it < m m = it;
return m;
}
clamp :: (x: $T, a: T, b: T) -> T
#modify { return IsScalar(T), "Argument is not of a scalar type."; }
{
if x < a x = a;
if x > b x = b;
return x;
}
swap :: inline (a: *$T, b: *T) {
// This is for when only want to mention each expression once instead of twice, because the names are long...
a.*, b.* = b.*, a.*;
}
// @Cleanup: We would like to do:
//
// talloc :: temporary_alloc;
//
// But this makes the code live right now. When we fix this part of the
// dead code elimination, we can do that...
talloc :: inline (size: s64) -> *void {
return temporary_allocator_proc(.ALLOCATE, size, 0, null, null);
}
temporary_alloc :: inline (size: s64) -> *void {
return temporary_allocator_proc(.ALLOCATE, size, 0, null, null);
}
temp :: Allocator.{temporary_allocator_proc, null};
temporary_allocator :: temp;
reset_temporary_storage :: () {
ts := context.temporary_storage;
if ts reset_temporary_storage(ts);
}
round :: (f: float32) -> s32 #expand {
return cast(s32) floor(f+0.5);
}
reset_struct :: Initialize;
Initialize :: (memory: *$T) {
ini :: initializer_of(T);
#if ini inline ini(memory);
else memset(memory, 0, size_of(T));
}
New :: ($T: Type, $initialized := true) -> *T {
memory := alloc(size_of(T));
// Since New gets called a lot, we just roll Initialize() into here, so that
// we don't kick off so many extra polymorphs.
#if initialized {
ini :: initializer_of(T);
#if ini inline ini(memory);
else memset(memory, 0, size_of(T));
}
return cast(*T) memory;
}
NewArray :: (count: s64, $T: Type, $initialized := true, alignment: s32 = -1) -> (result: [] T, unaligned_base: *void) {
if !count return .[], null;
// NewArray is for when we want to new a not-known-at-compile-time
// number of things...
a := context.allocator;
extra: s32 = 0;
if alignment > 0 {
extra = alignment;
}
// 'extra' lets us allocate extra memory so that the user can bump the pointer forward
// for alignment purposes.
// This is more versatile than requiring all allocators to support wacky alignments.
// This doesn't mean it's the best solution, but it is what we are doing for now.
original_memory := cast(*void) alloc(size_of(T) * count + extra);
if !original_memory return .[], null;
memory: *void;
if alignment > 0 {
memory = cast(*void) align_forward(cast(s64)original_memory, alignment);
} else {
memory = original_memory;
}
result: [] T = ---;
result.count = count;
result.data = memory;
#if initialized {
ini :: initializer_of(T);
#if ini {
for 0..count-1 {
inline ini(memory);
memory += size_of(T);
}
} else {
memset(memory, 0, count * size_of(T));
}
}
// If alignment == -1, you don't need to think about original_memory; you can just ignore it.
// You only need to care if alignment != -1; in that case, when it comes time to free the memory,
// you should free original_memory, not result.data.
return result, original_memory;
}
decimal_digits :: (v_in: s32) -> s32 {
v := v_in;
if (v < 0) v = -v; // ignore sign
if (v < 10) return 1;
if (v < 100) return 2;
if (v < 1000) return 3;
if (v < 10000) return 4;
if (v < 100000) return 5;
if (v < 1000000) return 6;
if (v < 10000000) return 7;
if (v < 100000000) return 8;
if (v < 1000000000) return 9;
return 10; // max for s32 (2,147,483,648)
}
// align_forward adds to orig_size so that it becomes an even multiple of 'alignment'.
align_forward :: (orig_size: s64, alignment: s64) -> s64 {
a := alignment;
assert(a > 0);
size := ((orig_size + a - 1) / a) * a;
// The above code might be hard to think about. Here's another implementation,
// that has a branch:
// size := orig_size;
// remainder := orig_size % a;
// if remainder size += a - remainder;
return size;
}
#if OS == {
case .WINDOWS; #load "windows.jai";
case .LINUX; #load "linux.jai";
case .ANDROID; #load "linux.jai";
case .MACOS; #load "osx.jai";
case .PS5; #load "ps5.jai";
case .NN_SWITCH; #load "nintendo.jai";
case .NN_SWITCH2; #load "nintendo.jai";
}
// pack_stack_trace is convenient in case you want to store the stack trace
// on a data structure. You can't just store a pointer to context.stack_trace,
// because that data will be destroyed as the stack unwinds. So this routine
// copies that trace into a packed array.
pack_stack_trace :: () -> [] Stack_Trace_Node {
return pack_stack_trace(context.stack_trace);
}
pack_stack_trace :: (node: *Stack_Trace_Node) -> [] Stack_Trace_Node {
// We could do a two-pass version where we array_reserve the right amount of data...!
result: [..] Stack_Trace_Node;
while node {
array_add(*result, node.*);
node = node.next;
}
// Fix up the 'next' pointers so you can use them normally.
for * result {
if it.next // The one that was already null, at the end, leave it null. Otherwise:
it.next = it + 1; // Set 'next' to point at the subsequent element in the array.
}
return result;
}
//
// print_stack_trace() can be used as a low-level routine for people writing
// Allocators, assertion handlers, loggers etc, and who are encountering
// bugs that they want help with. This is guaranteed not to call into
// any logger, allocator, etc because it uses only bottom-level routines.
//
// That said, you can also use it in other code!
//
//
// Higher-level routines can use log_stack_trace(), or get_stack_trace_strings(), which
// gives you strings you can render graphically, etc.
//
print_stack_trace :: (node: *Stack_Trace_Node, prefix: string = "", to_standard_error := true) {
while node {
if node.info {
if prefix.count write_string(prefix, to_standard_error = to_standard_error);
write_string("'", to_standard_error = to_standard_error);
if node.info.name write_string(node.info.name, to_standard_error = to_standard_error);
write_string("' at ", to_standard_error = to_standard_error);
if node.info.location.fully_pathed_filename write_string(node.info.location.fully_pathed_filename, to_standard_error = to_standard_error);
write_string(":", to_standard_error = to_standard_error);
write_nonnegative_number(node.line_number, to_standard_error = to_standard_error);
write_string("\n", to_standard_error = to_standard_error);
}
node = node.next;
}
}
log_stack_trace :: (node: *Stack_Trace_Node = null, log_flags := Log_Flags.ERROR, prefix := "") {
strings := get_stack_trace_strings(node);
for strings log("%1%2\n", prefix, it, flags = log_flags);
}
get_stack_trace_strings :: (node: *Stack_Trace_Node = null) -> [..] string /* Temporary_Storage */ {
if !node node = context.stack_trace;
results: [..] string;
results.allocator = temp;
while node {
if node.info {
s := sprint("'%' at %:%", node.info.name, node.info.location.fully_pathed_filename, node.line_number,, allocator=temp);
array_add(*results, s);
}
node = node.next;
}
return results;
}
remember_allocators :: (data: *$T) {
// In this language, we want the user of a function or library to be able to
// control that function's memory allocation using context.allocator.
//
// When you make a data structure that needs to allocate memory over a long
// period of time, though, this introduces a potential annoying constraint:
// if the user wants to store that data structure or pass it to some other code,
// that the user did not write, that other code will not know what allocator to push.
// So it's a good idea for data structures like this to store allocators at
// initialization time. (Data structures like Hash_Table and Bucket_Array do this;
// this is also why the language-native [..] array has allocator and allocator_data
// slots.)
//
// To make it convenient to save allocators in this way, we provide this function;
// it's a very simple function, but it helps people remember and continue the convention.
// All you need is to have 'allocator' and 'allocator_data' members, then call
// this routine.
if context.allocator.proc {
data.allocator = context.allocator;
} else {
data.allocator = context.default_allocator;
}
}
resizable :: (a: [] $T, set_allocators := true) -> [..] T {
// Convert a [] to a [..] that owns the same memory.
// The [..]'s allocated pointer will be set to a.count, so it will
// reallocate if you try to add any more.
// Since there is no way to know what the allocator of the [] was,
// we assume it's the current allocator, and remember that, unless you
// say set_allocators=false.
result: [..] T = ---;
result.count = a.count;
result.allocated = a.count;
result.data = a.data;
if set_allocators remember_allocators(*result);
return result;
}
get_integer_range :: ($T: Type) -> (low: T, high: T) {
// Returns the lowest and highest values for a given integer type.
// For example, u8 returns (0, 255); s8 returns (-127, 128).
IS_INTEGER, SIGNED, BITS :: #run is_integer_type(T);
#assert(IS_INTEGER);
#if SIGNED {
low := (cast(T)1) << (BITS-1);
} else {
low: T = 0;
}
return low, ~low;
}
clamp_to_another_integer_type :: (value: $SourceType, $TargetType: Type) -> TargetType {
// SourceType and TargetType
SOURCE_INTEGER, SOURCE_SIGNED, SOURCE_BITS :: #run is_integer_type(SourceType);
TARGET_INTEGER, TARGET_SIGNED, TARGET_BITS :: #run is_integer_type(TargetType);
#assert SOURCE_INTEGER && TARGET_INTEGER;
LOW, HIGH :: #run get_integer_range(TargetType);
#if SOURCE_SIGNED == TARGET_SIGNED {
// Both are signed, or both are unsigned.
#if TARGET_BITS < SOURCE_BITS {
if value < LOW value = LOW;
if value > HIGH value = HIGH;
}
return cast,no_check(TargetType) value;
} else {
// If we get here, SOURCE_SIGNED != TARGET_SIGNED.
#if SOURCE_SIGNED {
// TARGET_SIGNED == false.
if value < 0 value = 0;
#if TARGET_BITS < SOURCE_BITS {
if value > HIGH value = HIGH;
}
} else {
// SOURCE_SIGNED == false, TARGET_SIGNED == true.
#if TARGET_BITS <= SOURCE_BITS {
if value > HIGH value = HIGH;
}
}
return cast,no_check(TargetType) value;
}
}
is_integer_type :: ($T: Type) -> (is_integer: bool, is_signed: bool, bits: u8) {
info := type_info(T);
if info.type == .INTEGER {
info_int := cast(*Type_Info_Integer) info;
return true, info_int.signed, size_of(T)*8;
}
return false, false, 0;
}
seconds_since_init :: () -> float64 {
// This is the replacement for seconds_since_init().
one_time_init(*is_get_seconds_initialized, init_get_seconds());
delta := current_time_monotonic() - get_seconds_base_time;
return to_float64_seconds(delta);
}
#scope_file
is_get_seconds_initialized: s32;
get_seconds_base_time: Apollo_Time;
init_get_seconds :: () {
get_seconds_base_time = current_time_monotonic();
}
#scope_export
// Ok, so for many years kilo, mega and gigabytes meant 1024 * ..., but now
// we're supposed to use kibi, mibi and gibi??
Bytes :: (b: s64) -> s64 #expand { return b; }
KB :: Kilobytes;
Kilobytes :: (count: s64) -> s64 #expand { return count * 1024; }
MB :: Megabytes;
Megabytes :: (count: s64) -> s64 #expand { return count * 1_048_576; }
GB :: Gigabytes;
Gigabytes :: (count: s64) -> s64 #expand { return count * 1_073_741_824; }
TB :: Terabytes;
Terabytes :: (count: s64) -> s64 #expand { return count * 1_099_511_627_776; }
format_time_milliseconds :: (time: float64) -> string {
return tprint("% ms", FF(time * 1.0e3, 3));
}
format_delta_seconds :: inline (end_time: Apollo_Time, start_time: Apollo_Time) -> string {
return format_time_seconds(to_float64_seconds(end_time-start_time));
}
format_time_seconds :: (time: float64) -> string {
if time < 1.0e-6 then return tprint("% ns", FF(time * 1.0e9, 2));
if time < 1.0e-3 then return tprint("% us", FF(time * 1.0e6, 3));
if time < 1 then return tprint("% ms", FF(time * 1.0e3, 3));
return tprint("% s", FF(time, 2));
}
format_count :: (count: $T) -> string {
units : []string = .["", "thousand", "million", "billion", "trillion"];
count_f64 := cast(float64)count;
unit_index := 0;
while count_f64 >= 1000 && unit_index < (units.count - 1) {
count_f64 /= 1000.0;
unit_index += 1;
}
return tprint("% %", FF(count_f64, 2), units[unit_index]);
}
format_cycles :: (ticks: $T) -> string {
units : []string = .["cycles", "K cycles", "M cycles", "B cycles", "T cycles"];
count_f64 := cast(float64)ticks;
unit_index := 0;
while count_f64 >= 1000 && unit_index < (units.count - 1) {
count_f64 /= 1000.0;
unit_index += 1;
}
return tprint("% %", FF(count_f64, 2), units[unit_index]);
}
format_bytes :: (bytes_count: int) -> string {
units : []string = .["B", "KB", "MB", "GB", "TB", "PB"];
count_f64 := cast(float64)bytes_count;
unit_index := 0;
// Scale down until size < 1024 or we run out of units
while count_f64 >= 1024 && unit_index < (units.count - 1) {
count_f64 /= 1024.0;
unit_index += 1;
}
return tprint("% %", FF(count_f64, 2), units[unit_index]);
}

60
modules/Basic/osx.jai Normal file
View File

@ -0,0 +1,60 @@
#load "posix.jai";
store_in_integer :: (str: string) -> s32 {
assert(str.count <= 4);
// store contents of a string within an int
// attempt to mimick what C does with single-quote
// strings. For example:
// int c = 'sora';
// The value 0x736F7261 would be written to "c"
slice: [] u8;
slice.data = str.data;
slice.count = str.count;
out: s32;
for slice {
bits := ((slice.count - it_index) -1) * 8;
out = out | cast(s32) ((cast(u32) it) << bits);
}
return out;
}
// Returns a temporary string.
get_working_directory :: () -> string {
cwd := getcwd(null, 0);
defer c_free(cwd);
return copy_temporary_string(to_string(cwd));
}
// macOS API
IID :: struct {};
LPVOID :: *void;
AbsoluteTime :: u64;
_NSGetExecutablePath :: (buf: *u8, bufsize: *u32) -> s32 #foreign libc;
// this stuff has been deprecated for awhile it seems but Apple still lists this API as their recommended way
// to get GL addresses. see :NSGLGetProcAddress:
NSSymbol :: *void; // typedef'd to a pointer to an internal __NSSymbol struct
NSIsSymbolNameDefined :: (symbolName: *u8) -> bool #foreign libc #deprecated "use dlysym()";
NSLookupAndBindSymbol :: (symbolName: *u8) -> NSSymbol #foreign libc #deprecated "use dlysym()";
NSAddressOfSymbol :: (symbol: NSSymbol) -> *void #foreign libc #deprecated "use dlysym()";
FourCharCode :: u32;
OSStatus :: s32;
OSType :: FourCharCode;
noErr :OSStatus: 0;
#scope_file
libc :: #library,system "libc";
c_free :: (memory: *void) #foreign libc "free"; // :CFree:

59
modules/Basic/posix.jai Normal file
View File

@ -0,0 +1,59 @@
#import "POSIX";
sleep_milliseconds :: (milliseconds: s32) {
nanosleep :: (req: *timespec, rem: *timespec) -> int #foreign libc;
MILLI_TO_NANO :: 1000000;
MILLI_TO_SECS :: 1000;
req: timespec;
req.tv_sec = milliseconds / MILLI_TO_SECS;
req.tv_nsec = MILLI_TO_NANO * (milliseconds % MILLI_TO_SECS);
nanosleep(*req, null);
}
set_working_directory :: (s: string) -> bool {
chdir :: (s: *u8) -> s32 #foreign libc;
chdir_result := chdir(temp_c_string(s));
return chdir_result == 0;
}
exit :: (status: s32) {
// @Incomplete This would perform a series of C library termination procedures before ending the process.
// There's quick_exit, _Exit, etc. that perform minimal C lib procedures, but our goal will be to just
// not call this version since we'll be avoiding the CRT.
// @Incomplete: We lack a atexit() handlers, so any dynamic library that registers such procedure that is
// supposed to execute after exit() call, won't be called.
syscall_exit :: (syscall_number: u32, exit_code: s32) #expand {
#if CPU == .X64 {
#asm SYSCALL_SYSRET {
mov.q nr: gpr === a, syscall_number;
mov.d st: gpr === di, exit_code;
syscall o0:, o1:, nr, st;
}
} else {
syscall :: (__sysno: s64, __args: ..Any) -> s64 #foreign libc;
syscall(syscall_number, exit_code);
}
}
#if OS == {
case .LINUX;
syscall_exit(SYS_exit_group, status);
case .ANDROID;
syscall_exit(SYS_exit_group, status);
case .MACOS;
// SYSCALL_UNIX_BASE would not be necessary if we call libc’s syscall(), because it applies it automatically. But it doesn’t hurt either.
syscall_exit(SYSCALL_UNIX_BASE | SYS_exit, status);
case;
libc_exit :: (status: s32) #foreign libc "exit";
libc_exit(status);
}
}
#scope_file
libc :: #library,system "libc";

View File

@ -0,0 +1,144 @@
Deserializer :: struct { // downcasts to []u8
count: s64;
data: *u8;
cursor: s64;
ok := true; // cleared by any read that would run past the end; never set back
}
to_deserializer :: (view: $T/.[[]u8, string]) -> Deserializer #expand {
return .{view.count, view.data, 0, true};
}
deserializer_can_read :: inline (d: *Deserializer, byte_count: s64) -> bool {
if byte_count < 0 || d.cursor < 0 || d.cursor > d.count || byte_count > d.count - d.cursor {
d.ok = false;
return false;
}
return true;
}
Read :: inline (d: *Deserializer, $T: Type) -> T {
mem: T = ---;
if !deserializer_can_read(d, size_of(T)) {
Initialize(*mem); // a failed read yields a zero value
return mem;
}
memcpy(*mem, d.data + d.cursor, size_of(T));
d.cursor += size_of(T);
return mem;
}
ReadArrayView :: inline (d: *Deserializer, view_count: s64, $T: Type) -> []T {
// Range-check the count before multiplying, so a hostile count cannot overflow it.
if view_count < 0 || d.cursor < 0 || d.cursor > d.count
|| view_count > (d.count - d.cursor) / size_of(T) {
d.ok = false;
return .[];
}
array: []T = .{view_count, cast(*T)(d.data + d.cursor)};
d.cursor += view_count * size_of(T);
return array;
}
ReadSizedArray :: inline (d: *Deserializer, $T: Type, $count_type: Type = s64) -> []T {
view_count := xx Read(d, count_type);
return ReadArrayView(d, view_count, T);
}
ReadStringView :: inline (d: *Deserializer, view_count: s64) -> string {
if !deserializer_can_read(d, view_count) return "";
s := string.{view_count, d.data + d.cursor};
d.cursor += view_count;
return s;
}
ReadSizedString :: inline (d: *Deserializer, $count_type: Type = s64) -> string {
view_count : s64 = xx Read(d, count_type);
return ReadStringView(d, view_count);
}
// Use Resizable_Array APIs directly: e.g. array_reset_keeping_memory, array_free, array_reset...
Serializer :: [..]u8;
new_serializer :: inline (reserve_count: s64) -> Serializer {
serializer: Serializer;
array_reserve(*serializer, reserve_count);
return serializer;
}
Add :: inline (s: *Serializer, item: $T) {
// we use the offset, do NOT use s.data, because the pointer
// can move on resize, so it is no longer valid!
starting_offset := s.count;
array_resize(s, s.count + size_of(T));
memcpy(s.data + starting_offset, *item, size_of(T));
}
AddArray_NoSize :: inline (s: *Serializer, view: []$T) {
starting_offset := s.count;
array_resize(s, s.count + (view.count * size_of(T)));
memcpy(s.data + starting_offset, view.data, view.count * size_of(T));
}
AddSizedArray :: inline (s: *Serializer, view: []$T, $count_type: Type = s64) -> bool {
// A count that does not survive count_type would be written truncated and then followed by
// the full payload, so every field after it reads from the wrong offset. The round trip
// catches that for any count_type; cast,no_check keeps a debug build from aborting on the
// cast before this can report it.
view_count := cast,no_check(count_type) view.count;
if cast(s64) view_count != view.count {
log_error("[AddSizedArray] a % element array does not fit a % length prefix.", view.count, count_type);
return false;
}
Add(s, view_count);
AddArray_NoSize(s, view);
return true;
}
AddString :: inline (s: *Serializer, view: string, $count_type: Type = s64) -> bool {
view_count := cast,no_check(count_type) view.count;
if cast(s64) view_count != view.count {
log_error("[AddString] a % byte string does not fit a % length prefix.", view.count, count_type);
return false;
}
Add(s, view_count);
AddArray_NoSize(s, cast([]u8)view);
return true;
}
// This has specialized use cases where the offsets are saved in a separate array. e.g. SoA filesystem string storage.
AddString_NoSize :: inline (s: *Serializer, data: *u8, count: $T, $offset_type: Type = s32) -> offset_type {
offset := cast(offset_type) s.count;
AddArray_NoSize(s, []u8.{count, data});
return offset;
}
AddString_PaddedTo8ByteAlignment :: (s: *Serializer, view: string) {
// The purpose of this function is to zero-pad a string to
// 8-bytes so we don't run into annoying unaligned copy issues.
Add(s, view.count);
AddArray_NoSize(s, cast([]u8)view);
starting_offset := s.count;
remainder := align_forward(view.count, size_of(s64)) - view.count;
array_resize(s, s.count + remainder);
// Fill remaining space with zeros
memset(s.data + starting_offset, 0, remainder);
}

File diff suppressed because it is too large Load Diff

713
modules/Basic/tests.jai Normal file
View File

@ -0,0 +1,713 @@
print_int_tests :: () {
assert(tprint("%", 0) == "0", "0: Plain failed");
assert(tprint("%", formatInt(0)) == "0", "0: Default failed");
assert(tprint("%", formatInt(0, minimum_digits = 0)) == "", "0: Explicit 0 failed");
assert(tprint("%", formatInt(0, minimum_digits = 1)) == "0", "0: Explicit 1 failed");
assert(tprint("%", formatInt(0, minimum_digits = 3)) == "000", "0: Explicit 3 failed");
assert(tprint("%", 25) == "25", "25: Plain failed");
assert(tprint("%", formatInt(25)) == "25", "25: Default failed");
assert(tprint("%", formatInt(25, minimum_digits = 3)) == "025", "25: Explicit 3 failed");
assert(tprint("%", formatInt(25, minimum_digits = 1)) == "25", "25: Explicit 1 failed");
assert(tprint("%", formatInt(25, minimum_digits = 0)) == "25", "25: Explicit 0 failed");
assert(tprint("%", formatInt(1234, minimum_digits = 0)) == "1234", "1234: Explicit 0 failed");
assert(tprint("%", formatInt(1234, minimum_digits = 1)) == "1234", "1234: Explicit 1 failed");
} @test
print_int_hex_tests :: () {
assert(tprint("%", formatInt(25, base = 16)) == "19", "Default failed");
assert(tprint("%", formatInt(25, base = 16, minimum_digits = 3)) == "019", "3 failed");
assert(tprint("%", formatInt(25, base = 16, minimum_digits = 1)) == "19", "1 failed");
}
print_float_tests :: () {
assert(tprint("%", 1234.5) == "1234.5", "Plain failed");
assert(tprint("%", formatFloat(1234.5)) == "1234.5", "Default failed");
assert(tprint("%", formatFloat(1234.5, trailing_width = 1)) == "1234.5", "Traling 1 failed");
assert(tprint("%", formatFloat(1234.5, trailing_width = 4, zero_removal = .NO)) == "1234.5000", "Traling 4 no_removal failed");
assert(tprint("%", formatFloat(1234.5, trailing_width = 4)) == "1234.5 ", "Traling 4 failed");
assert(tprint("%", formatFloat(1234.5, mode = .SHORTEST)) == "1234.5", "Shortest 1 failed");
assert(tprint("%", formatFloat(60.0, mode = .SHORTEST)) == "60", "Shortest 2 failed");
assert(tprint("%", formatFloat(60.0, mode = .SHORTEST, zero_removal = .NO)) == "60", "Shortest without zero removal failed");
assert(tprint("%", formatFloat(2.0e-31, mode = .SHORTEST)) == "2e-31", "Shortest scientific failed");
print("%", formatFloat(60.0, trailing_width=2, mode = .SCIENTIFIC));
{
scientific := tprint("%", formatFloat(60.0, mode = .SCIENTIFIC, zero_removal = .NO));
EXPECTED_SCIENTIFIC :: "6.000000e+01";
assert(scientific == EXPECTED_SCIENTIFIC, "Scientific w/o zero removal failed: % vs %", scientific, EXPECTED_SCIENTIFIC);
}
// @ToDo: This one fails for the old implementation but there’s no point fixing that if we plan to ditch it very soon.
// Enable this test when we switch to the new implementation.
// -rluba, 2023-08-07
// {
// scientific := tprint("%", formatFloat(60.0, mode = .SCIENTIFIC));
// EXPECTED_SCIENTIFIC :: "6e+01";
// assert(scientific == EXPECTED_SCIENTIFIC, "Scientific with zero removal failed: % vs %", scientific, EXPECTED_SCIENTIFIC);
// }
print("%\n", formatFloat(8.314472e0, mode = .SCIENTIFIC));
print("%\n", formatFloat(8.314472e0, mode = .SCIENTIFIC, zero_removal = .NO));
assert(tprint("%", formatFloat(8.314472e0, mode = .SCIENTIFIC)) == "8.314472e+00", "Scientific 2 failed");
}
simple_string_tests :: () { // Very simple, to get the ball rolling.
SIZE :: 15;
s1 : [SIZE] u8;
memcpy(s1.data, "Hello, Sailor!".data, SIZE-1);
s1[SIZE-1] = 0;
// @todo @abner use temporary_storage too and add is_* tests (but those may be moved out of Basic)
// @Cleanup: These asserts are awful, wtf.
assert(c_style_strlen(s1.data) == SIZE-1);
assert(cast(string)s1 == "Hello, Sailor!\0");
assert(to_string(s1.data) == "Hello, Sailor!");
assert(to_string(s1.data, SIZE-1) == "Hello, Sailor!");
s2 : [..] u8;
for index : 0..SIZE-2 array_add(*s2, s1[index]); // Inefficient, but uses array_add which is part of Basic so it's good to test.
assert(cast(string)s2 == "Hello, Sailor!");
s3 := cast(string) s2;
s3_c_string := to_c_string(s3);
defer free(s3_c_string);
assert(c_style_strlen(s3_c_string) == SIZE-1);
assert(memcmp(s3_c_string, s3.data, SIZE-1) == 0);
advance(*s3, 0);
assert(s3[0] == #char "H", message="s3[0] (%) == #char \"H\"", s3[0]);
advance(*s3, SIZE-2);
assert(s3[0] == #char "!", message="s3[0] (%) == #char \"!\"", s3[0]);
}
string_builder_tests :: () { // Very simple, to get the ball rolling.
builder : String_Builder;
init_string_builder(*builder);
assert(get_current_buffer(*builder).count == 0);
assert(builder_to_string(*builder) == "");
assert(builder_string_length(*builder) == 0);
//
// Append nothing, string literals, and u8s.
//
// @todo @abner test string builder's buffer swap strategy; so if we append something bigger than BUFFER_SIZE
// we could assert current_buffer is a new address.
append(*builder, "");
{
output := builder_to_string(*builder);
defer free(output);
length := builder_string_length(*builder);
assert(get_current_buffer(*builder).count == 0, message="get_current_buffer(*builder).count (%) == 0", get_current_buffer(*builder).count);
assert(output == "", message="builder_to_string(*builder) (%) == \"\"", output);
assert(length == 0, message="builder_string_length(*builder) (%) == 0", length);
}
append(*builder, "Hello");
{
output := builder_to_string(*builder, do_reset=false);
defer free(output);
length := builder_string_length(*builder);
assert(output == "Hello", message="builder_to_string(*builder) (\"%\") == \"%\"", builder_to_string(*builder, do_reset=false), "Hello");
assert(length == 5, message="builder_string_length(*builder) (%) == %", length, 5);
assert(get_current_buffer(*builder).count == length, message="get_current_buffer(*builder).count (%) == %", get_current_buffer(*builder).count, length);
}
append(*builder, ", ");
{
output := builder_to_string(*builder, do_reset=false);
defer free(output);
length := builder_string_length(*builder);
assert(output == "Hello, ", message="builder_to_string(*builder) (\"%\") == \"%\"", output, "Hello, ");
assert(length == 7, message="builder_string_length(*builder) (%) == %", length, 7);
assert(get_current_buffer(*builder).count == length, message="get_current_buffer(*builder).count (%) == %", get_current_buffer(*builder).count, length);
}
append(*builder, "Sailor!");
{
output := builder_to_string(*builder, do_reset=false);
defer free(output);
length := builder_string_length(*builder);
assert(output == "Hello, Sailor!", message="builder_to_string(*builder) (\"%\") == \"%\"", output, "Hello, Sailor!");
assert(length == 14, message="builder_string_length(*builder) (%) == %", length, 14);
assert(get_current_buffer(*builder).count == 14, message="get_current_buffer(*builder).count (%) == %", get_current_buffer(*builder).count, 14);
}
SIZE :: 13;
goodbye : [SIZE] u8;
memcpy(goodbye.data, " Goodbye now.".data, SIZE);
append(*builder, goodbye.data, SIZE);
{
output := builder_to_string(*builder, do_reset=false);
defer free(output);
length := builder_string_length(*builder);
assert(output == "Hello, Sailor! Goodbye now.", message="builder_to_string(*builder) (\"%\") == \"%\"", output, "Hello, Sailor! Goodbye now.");
assert(length == 14+SIZE, message="builder_string_length(*builder) (%) == %", length, 14+SIZE);
assert(get_current_buffer(*builder).count == 14+SIZE, message="get_current_buffer(*builder).count (%) == %", get_current_buffer(*builder).count, 14+SIZE);
}
//
// Reset
//
reset(*builder);
{
output := builder_to_string(*builder);
defer free(output);
length := builder_string_length(*builder);
assert(get_current_buffer(*builder).count == 0, message="get_current_buffer(*builder).count (%) == 0", get_current_buffer(*builder).count);
assert(output == "", message="builder_to_string(*builder) (%) == \"\"", output);
assert(length == 0, message="builder_string_length(*builder) (%) == 0", length);
}
// Stealing
{
victim: String_Builder;
thief: String_Builder;
append(*victim, "First line\n");
old_victim_count := builder_string_length(*victim);
append_and_steal_buffers(*thief, *victim);
log("Result after heist #1: %", builder_to_string(*thief, do_reset = false));
thief_count := builder_string_length(*thief);
assert(thief_count == old_victim_count);
assert(builder_string_length(*victim) == 0);
// The first one only copied from the base buffer.
// Let’s try actual stealing:
old_thief_count := thief_count;
for 1..100 {
print_to_builder(*victim, "This will be stolen next and we want multiple buffers, part %\n", it);
}
old_victim_count = builder_string_length(*victim);
append_and_steal_buffers(*thief, *victim);
log("\n\nResult after heist #2: %", builder_to_string(*thief, do_reset = false));
thief_count = builder_string_length(*thief);
assert(thief_count == old_victim_count + old_thief_count);
assert(builder_string_length(*victim) == 0);
// Make sure that we can append normally after stealing from victim a bunch:
old_thief_count = thief_count;
INTERMISSION :: "Appended between heist jobs\n";
print_to_builder(*thief, INTERMISSION);
log("\n\nResult after intermission: %", builder_to_string(*thief, do_reset = false));
thief_count = builder_string_length(*thief);
assert(thief_count == old_thief_count + INTERMISSION.count);
// Try stealing some more now that thief also has multiple buffers:
old_thief_count = thief_count;
for 1..100 {
print_to_builder(*victim, "This will be stolen again and we want to make sure we have multiple buffers, part %\n", it);
}
old_victim_count = builder_string_length(*victim);
append_and_steal_buffers(*thief, *victim);
log("\n\nResult after heist #3: %", builder_to_string(*thief, do_reset = false));
thief_count = builder_string_length(*thief);
assert(thief_count == old_victim_count + old_thief_count);
assert(builder_string_length(*victim) == 0);
}
} @test
copy_string_builder_tests :: () {
{
builder := create_builder("What", "we", "expected", init = false);
print("Builder %, outer current buffer: %\n", *builder, builder.current_buffer);
stuff := do_other_stack_stuff();
assert(stuff[0] == 0); // Just so the optimizer doesn't get rid of it
assert(stuff[1023] == 255); // Just so the optimizer doesn't get rid of it
output := builder_to_string(*builder);
print("Outer output: %\n", output);
assert(output == "What we expected");
}
{
builder := create_builder("What", "we", "expected", init = true);
print("Builder %, outer current buffer: %\n", *builder, builder.current_buffer);
stuff := do_other_stack_stuff();
assert(stuff[0] == 0); // Just so the optimizer doesn't get rid of it
assert(stuff[1023] == 255); // Just so the optimizer doesn't get rid of it
output := builder_to_string(*builder);
print("Outer output: %\n", output);
assert(output == "What we expected");
}
print("OK\n");
}
apollo_time_tests :: () {
#if OS != .WINDOWS {
unix_epoch_start := timespec_to_apollo(.{0, 0});
ct := to_calendar(unix_epoch_start, .UTC);
log("Unix epoch start: %", ct);
assert(ct.year == 1970);
assert(ct.month_starting_at_0 == 0);
assert(ct.day_of_month_starting_at_0 == 0);
assert(ct.day_of_week_starting_at_0 == 4);
assert(ct.hour == 0);
assert(ct.minute == 0);
assert(ct.second == 0);
assert(ct.millisecond == 0);
assert(ct.time_zone == .UTC);
}
{
ct: Calendar_Time;
ct.year = 2022;
ct.month_starting_at_0 = 3;
ct.day_of_month_starting_at_0 = 5;
ct.hour = 16;
ct.minute = 26;
ct.second = 30;
ct.millisecond = 500;
ct.time_zone = .LOCAL;
apollo_time := calendar_to_apollo(ct);
ct_roundtrip := to_calendar(apollo_time, ct.time_zone);
log("Calendar to Apollo to Calendar roundtrip for %: %", ct, ct_roundtrip);
assert(ct_roundtrip.year == 2022);
assert(ct_roundtrip.month_starting_at_0 == 3);
assert(ct_roundtrip.day_of_month_starting_at_0 == 5);
assert(ct_roundtrip.hour == 16);
assert(ct_roundtrip.minute == 26);
assert(ct_roundtrip.second == 30);
assert(ct_roundtrip.millisecond == 500);
assert(ct_roundtrip.time_zone == .LOCAL);
}
{
ct: Calendar_Time;
ct.year = 2022;
ct.month_starting_at_0 = 3;
ct.day_of_month_starting_at_0 = 5;
ct.hour = 16;
ct.minute = 26;
ct.second = 30;
ct.millisecond = 500;
ct.time_zone = .UTC;
apollo_time := calendar_to_apollo(ct);
ct_roundtrip := to_calendar(apollo_time, ct.time_zone);
log("Calendar to Apollo to Calendar roundtrip for %: %", ct, ct_roundtrip);
assert(ct_roundtrip.year == 2022);
assert(ct_roundtrip.month_starting_at_0 == 3);
assert(ct_roundtrip.day_of_month_starting_at_0 == 5);
assert(ct_roundtrip.hour == 16);
assert(ct_roundtrip.minute == 26);
assert(ct_roundtrip.second == 30);
assert(ct_roundtrip.millisecond == 500);
assert(ct_roundtrip.time_zone == .UTC);
}
} @test
create_builder :: (strings: .. string, init: bool) -> String_Builder {
builder: String_Builder;
if init init_string_builder(*builder);
for strings {
if it_index {
append(*builder, " ");
}
append(*builder, it);
}
print("Current buffer: %\n", builder.current_buffer);
output := builder_to_string(*builder, do_reset=false);
print("Inner output: %\n", output);
return builder;
}
do_other_stack_stuff :: () -> [1024] u8 {
my_array: [1024] u8;
for 0..my_array.count - 1 {
my_array[it] = xx (it % 256);
}
return my_array;
}
parse_int_tests :: () {
result_10 := string_to_int("105", 10, u8);
assert(result_10 == 105);
result_16 := string_to_int("105", 16, u16);
assert(result_16 == 0x105);
result_8 := string_to_int("105", 8, u8);
assert(result_8 == 69);
result_2 := string_to_int("101", 2, u8);
assert(result_2 == 0b101);
}
parse_float_tests :: () {
// Check that parsing denormalized float values is fine. This is the minimum positive subnormal value:
parsed_value, success, remainder := string_to_float64("4.9406564584124654e-324");
print("Result: % % %", parsed_value, success, remainder);
assert(success == true);
assert(parsed_value == 0h0000_0000_0000_0001, "Unexpected parsed value: 0h%", formatInt((.*)(cast(*u64) *parsed_value), base=16, minimum_digits = 16, digits_per_comma = 4, comma_string = "_"));
}
test_single_apollo_time_conversion :: (time: Apollo_Time, factor: u64, $round: bool, expected_result: s64, expected_success: bool) -> fail: int {
to_string :: (x: S128) -> string {
chars: [39] u8;
count := 0;
n := x;
while n != .{} {
n=, digit := signed_divide_with_remainder(n, .{10, 0});
if digit == S128_ERROR return "<overflow>";
chars[count] = cast,trunc(u8)(digit.low) + #char "0";
count += 1;
}
builder: String_Builder;
if x < .{} append(*builder, "-");
for < 0..count - 1 append(*builder, chars[it]);
return builder_to_string(*builder);
}
result, success := ConvertFromApollo(time, factor, round);
error := false;
if success {
print("% % / % = % ", ifx round then "^" else " ", to_string(time), factor, result);
if expected_success error = expected_result != result;
else error = true;
} else {
print("% % / % = % ", ifx round then "^" else " ", to_string(time), factor, "<OVERFLOW>");
error = expected_success;
}
if error print_color("FAIL\n", color = .RED);
else print_color("OK\n", color = .GREEN);
return ifx error then 1 else 0;
}
test_apollo_time_conversion :: () {
test :: (time: Apollo_Time, factor: u64, expected_result: s64) -> fail: int {
return test_single_apollo_time_conversion(time, factor, false, expected_result, true);
}
test_round :: (time: Apollo_Time, factor: u64, expected_result: s64) -> fail: int {
return test_single_apollo_time_conversion(time, factor, true, expected_result, true);
}
test :: (time: Apollo_Time, factor: u64, expected_success: bool) -> fail: int {
return test_single_apollo_time_conversion(time, factor, false, 0, expected_success);
}
test_round :: (time: Apollo_Time, factor: u64, expected_success: bool) -> fail: int {
return test_single_apollo_time_conversion(time, factor, true, 0, expected_success);
}
fails := 0;
print("\nApollo Time Conversion:\n\n");
// expected values ascertained from wolfram alpha
plus_one := Apollo_Time.{low = 1, high = 0};
fails += test(plus_one, 1, 1);
fails += test(plus_one, 2, 0);
fails += test(plus_one, 3, 0);
fails += test(plus_one, 4, 0);
fails += test(plus_one, 5, 0);
fails += test(plus_one, xx S64_MAX, 0);
fails += test(plus_one, 0x80000000_00000000, 0);
fails += test(plus_one, U64_MAX, 0);
print("\n");
fails += test_round(plus_one, 1, 1);
fails += test_round(plus_one, 2, 1);
fails += test_round(plus_one, 3, 0);
fails += test_round(plus_one, 4, 0);
fails += test_round(plus_one, 5, 0);
fails += test_round(plus_one, xx S64_MAX, 0);
fails += test_round(plus_one, 0x80000000_00000000, 0);
fails += test_round(plus_one, U64_MAX, 0);
print("\n");
fails += test(-plus_one, 1, -1);
fails += test(-plus_one, 2, 0);
fails += test(-plus_one, 3, 0);
fails += test(-plus_one, 4, 0);
fails += test(-plus_one, 5, 0);
fails += test(-plus_one, xx S64_MAX, 0);
fails += test(-plus_one, 0x80000000_00000000, 0);
fails += test(-plus_one, U64_MAX, 0);
print("\n");
fails += test_round(-plus_one, 1, -1);
fails += test_round(-plus_one, 2, 0);
fails += test_round(-plus_one, 3, 0);
fails += test_round(-plus_one, 4, 0);
fails += test_round(-plus_one, 5, 0);
fails += test_round(-plus_one, xx S64_MAX, 0);
fails += test_round(-plus_one, 0x80000000_00000000, 0);
fails += test_round(-plus_one, U64_MAX, 0);
print("\n");
plus_two := Apollo_Time.{low = 2, high = 0};
fails += test(plus_two, 1, 2);
fails += test(plus_two, 2, 1);
fails += test(plus_two, 3, 0);
fails += test(plus_two, 4, 0);
fails += test(plus_two, 5, 0);
fails += test(plus_two, xx S64_MAX, 0);
fails += test(plus_two, 0x80000000_00000000, 0);
fails += test(plus_two, U64_MAX, 0);
print("\n");
fails += test_round(plus_two, 1, 2);
fails += test_round(plus_two, 2, 1);
fails += test_round(plus_two, 3, 1);
fails += test_round(plus_two, 4, 1);
fails += test_round(plus_two, 5, 0);
fails += test_round(plus_two, xx S64_MAX, 0);
fails += test_round(plus_two, 0x80000000_00000000, 0);
fails += test_round(plus_two, U64_MAX, 0);
print("\n");
fails += test(-plus_two, 1, -2);
fails += test(-plus_two, 2, -1);
fails += test(-plus_two, 3, 0);
fails += test(-plus_two, 4, 0);
fails += test(-plus_two, 5, 0);
fails += test(-plus_two, xx S64_MAX, 0);
fails += test(-plus_two, 0x80000000_00000000, 0);
fails += test(-plus_two, U64_MAX, 0);
print("\n");
fails += test_round(-plus_two, 1, -2);
fails += test_round(-plus_two, 2, -1);
fails += test_round(-plus_two, 3, -1);
fails += test_round(-plus_two, 4, 0);
fails += test_round(-plus_two, 5, 0);
fails += test_round(-plus_two, xx S64_MAX, 0);
fails += test_round(-plus_two, 0x80000000_00000000, 0);
fails += test_round(-plus_two, U64_MAX, 0);
print("\n");
// 9223372036854775807
s64_max := Apollo_Time.{low = xx S64_MAX, high = 0};
fails += test(s64_max, 1, 9223372036854775807);
fails += test(s64_max, 2, 4611686018427387903);
fails += test(s64_max, 3, 3074457345618258602);
fails += test(s64_max, 4, 2305843009213693951);
fails += test(s64_max, 5, 1844674407370955161);
fails += test(s64_max, xx S64_MAX, 1);
fails += test(s64_max, 0x80000000_00000000, 0);
fails += test(s64_max, U64_MAX, 0);
print("\n");
fails += test_round(s64_max, 1, 9223372036854775807);
fails += test_round(s64_max, 2, 4611686018427387904);
fails += test_round(s64_max, 3, 3074457345618258602);
fails += test_round(s64_max, 4, 2305843009213693952);
fails += test_round(s64_max, 5, 1844674407370955161);
fails += test_round(s64_max, xx S64_MAX, 1);
fails += test_round(s64_max, 0x80000000_00000000, 1);
fails += test_round(s64_max, U64_MAX, 0);
print("\n");
// -9223372036854775807
fails += test(-s64_max, 1, -9223372036854775807);
fails += test(-s64_max, 2, -4611686018427387903);
fails += test(-s64_max, 3, -3074457345618258602);
fails += test(-s64_max, 4, -2305843009213693951);
fails += test(-s64_max, 5, -1844674407370955161);
fails += test(-s64_max, xx S64_MAX, -1);
fails += test(-s64_max, 0x80000000_00000000, 0);
fails += test(-s64_max, U64_MAX, 0);
print("\n");
fails += test_round(-s64_max, 1, -9223372036854775807);
fails += test_round(-s64_max, 2, -4611686018427387903);
fails += test_round(-s64_max, 3, -3074457345618258602);
fails += test_round(-s64_max, 4, -2305843009213693952);
fails += test_round(-s64_max, 5, -1844674407370955161);
fails += test_round(-s64_max, xx S64_MAX, -1);
fails += test_round(-s64_max, 0x80000000_00000000, -1);
fails += test_round(-s64_max, U64_MAX, 0);
print("\n");
// 9223372036854775808
s64_max_plus_one := s64_max + .{1, 0};
fails += test(s64_max_plus_one, 1, false);
fails += test(s64_max_plus_one, 2, 4611686018427387904);
fails += test(s64_max_plus_one, 3, 3074457345618258602);
fails += test(s64_max_plus_one, 4, 2305843009213693952);
fails += test(s64_max_plus_one, 5, 1844674407370955161);
fails += test(s64_max_plus_one, xx S64_MAX, 1);
fails += test(s64_max_plus_one, 0x80000000_00000000, 1);
fails += test(s64_max_plus_one, U64_MAX, 0);
print("\n");
fails += test_round(s64_max_plus_one, 1, false);
fails += test_round(s64_max_plus_one, 2, 4611686018427387904);
fails += test_round(s64_max_plus_one, 3, 3074457345618258603);
fails += test_round(s64_max_plus_one, 4, 2305843009213693952);
fails += test_round(s64_max_plus_one, 5, 1844674407370955162);
fails += test_round(s64_max_plus_one, xx S64_MAX, 1);
fails += test_round(s64_max_plus_one, 0x80000000_00000000, 1);
fails += test_round(s64_max_plus_one, U64_MAX, 1);
print("\n");
// -9223372036854775808
fails += test(-s64_max_plus_one, 1, -9223372036854775808);
fails += test(-s64_max_plus_one, 2, -4611686018427387904);
fails += test(-s64_max_plus_one, 3, -3074457345618258602);
fails += test(-s64_max_plus_one, 4, -2305843009213693952);
fails += test(-s64_max_plus_one, 5, -1844674407370955161);
fails += test(-s64_max_plus_one, xx S64_MAX, -1);
fails += test(-s64_max_plus_one, 0x80000000_00000000, -1);
fails += test(-s64_max_plus_one, U64_MAX, 0);
print("\n");
fails += test_round(-s64_max_plus_one, 1, -9223372036854775808);
fails += test_round(-s64_max_plus_one, 2, -4611686018427387904);
fails += test_round(-s64_max_plus_one, 3, -3074457345618258603);
fails += test_round(-s64_max_plus_one, 4, -2305843009213693952);
fails += test_round(-s64_max_plus_one, 5, -1844674407370955162);
fails += test_round(-s64_max_plus_one, xx S64_MAX, -1);
fails += test_round(-s64_max_plus_one, 0x80000000_00000000, -1);
fails += test_round(-s64_max_plus_one, U64_MAX, -1);
print("\n");
// 18446744073709551615
u64_max := Apollo_Time.{low = U64_MAX, high = 0};
fails += test(u64_max, 1, false);
fails += test(u64_max, 2, 9223372036854775807);
fails += test(u64_max, 3, 6148914691236517205);
fails += test(u64_max, 4, 4611686018427387903);
fails += test(u64_max, 5, 3689348814741910323);
fails += test(u64_max, xx S64_MAX, 2);
fails += test(u64_max, 0x80000000_00000000, 1);
fails += test(u64_max, U64_MAX, 1);
print("\n");
fails += test_round(u64_max, 1, false);
fails += test_round(u64_max, 2, false);
fails += test_round(u64_max, 3, 6148914691236517205);
fails += test_round(u64_max, 4, 4611686018427387904);
fails += test_round(u64_max, 5, 3689348814741910323);
fails += test_round(u64_max, xx S64_MAX, 2);
fails += test_round(u64_max, 0x80000000_00000000, 2);
fails += test_round(u64_max, U64_MAX, 1);
print("\n");
// -18446744073709551615
fails += test(-u64_max, 1, false);
fails += test(-u64_max, 2, -9223372036854775807);
fails += test(-u64_max, 3, -6148914691236517205);
fails += test(-u64_max, 4, -4611686018427387903);
fails += test(-u64_max, 5, -3689348814741910323);
fails += test(-u64_max, xx S64_MAX, -2);
fails += test(-u64_max, 0x80000000_00000000, -1);
fails += test(-u64_max, U64_MAX, -1);
print("\n");
fails += test_round(-u64_max, 1, false);
fails += test_round(-u64_max, 2, -9223372036854775807);
fails += test_round(-u64_max, 3, -6148914691236517205);
fails += test_round(-u64_max, 4, -4611686018427387904);
fails += test_round(-u64_max, 5, -3689348814741910323);
fails += test_round(-u64_max, xx S64_MAX, -2);
fails += test_round(-u64_max, 0x80000000_00000000, -2);
fails += test_round(-u64_max, U64_MAX, -1);
print("\n");
// 36893488147419103230
double_u64_max := u64_max * .{2, 0};
fails += test(double_u64_max, 1, false);
fails += test(double_u64_max, 2, false);
fails += test(double_u64_max, 3, false);
fails += test(double_u64_max, 4, 9223372036854775807);
fails += test(double_u64_max, 5, 7378697629483820646);
fails += test(double_u64_max, xx S64_MAX, 4);
fails += test(double_u64_max, 0x80000000_00000000, 3);
fails += test(double_u64_max, U64_MAX, 2);
print("\n");
fails += test_round(double_u64_max, 1, false);
fails += test_round(double_u64_max, 2, false);
fails += test_round(double_u64_max, 3, false);
fails += test_round(double_u64_max, 4, false);
fails += test_round(double_u64_max, 5, 7378697629483820646);
fails += test_round(double_u64_max, xx S64_MAX, 4);
fails += test_round(double_u64_max, 0x80000000_00000000, 4);
fails += test_round(double_u64_max, U64_MAX, 2);
print("\n");
// -36893488147419103230
fails += test(-double_u64_max, 1, false);
fails += test(-double_u64_max, 2, false);
fails += test(-double_u64_max, 3, false);
fails += test(-double_u64_max, 4, -9223372036854775807);
fails += test(-double_u64_max, 5, -7378697629483820646);
fails += test(-double_u64_max, xx S64_MAX, -4);
fails += test(-double_u64_max, 0x80000000_00000000, -3);
fails += test(-double_u64_max, U64_MAX, -2);
print("\n");
fails += test_round(-double_u64_max, 1, false);
fails += test_round(-double_u64_max, 2, false);
fails += test_round(-double_u64_max, 3, false);
fails += test_round(-double_u64_max, 4, -9223372036854775807);
fails += test_round(-double_u64_max, 5, -7378697629483820646);
fails += test_round(-double_u64_max, xx S64_MAX, -4);
fails += test_round(-double_u64_max, 0x80000000_00000000, -4);
fails += test_round(-double_u64_max, U64_MAX, -2);
print("\n");
if fails print("% FAILED TESTS!\n", fails);
else print("ALL TESTS PASSED\n");
}
#import "Basic";
#import "Math";
#import "Print_Color";
#import "Random";
main :: () {
print_int_tests();
print_float_tests();
simple_string_tests();
string_builder_tests();
apollo_time_tests();
parse_int_tests();
parse_float_tests();
seed := random_get();
print("\n\nRandom seed: %\n", seed);
random_seed(seed);
#if CPU == .X64 {
test_unsigned_division(10000, random_get);
}
test_apollo_time_conversion();
}
// @ToDo @Fixme: parse_float_tests() fails when running at compile time, so there is something very sus going on…
// #run main();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,925 @@
// Test suite for fixed_arena.jai.
//
// module.jai #loads this file when Basic is imported with LOAD_TESTS = true, so it and the
// module are one compilation unit and the #scope_module machinery (Fixed_Arena_State,
// get_fixed_arena_state, restore_fixed_arena_state) is in reach. It is still exercised
// through auto_reset on purpose, because the public contract is what callers depend on.
//
// Covers every exported symbol of the file.
//
// Groups are toggled with the RUN_* constants below. Each group is #if'd at its call site
// in run_fixed_arena_tests(), so a disabled group is not invoked at all. Structure follows
// tests/arena_tests.jai deliberately -- same harness, same reporting, same conventions --
// so that the two suites read as one thing.
//
// Several groups deliberately drive failure paths that log. Expected log_error output is
// bracketed by "expecting errors" banners -- noise inside a banner is the test working,
// noise outside one is not.
//
// Each test names the review finding it guards, so a failure points at the reasoning as
// well as the line. See fixed-arena-code-review.html.
#scope_file
// ---------------------------------------------------------------------------
// Group switches
// ---------------------------------------------------------------------------
RUN_LIFECYCLE_TESTS :: true; // bootstrap_fixed_arena, fixed_arena_init, delete, is_valid
RUN_ALLOCATION_TESTS :: true; // fixed_arena_alloc happy paths, cursor bookkeeping
RUN_BOUNDS_TESTS :: true; // exhaustion, exact capacity, negative and overflowing sizes
RUN_ALIGNMENT_TESTS :: true; // every result aligned as an address, not as an offset
RUN_RESIZE_TESTS :: true; // in-place growth and shrink, copy fallback, null old_memory
RUN_RESET_TESTS :: true; // fixed_arena_reset, auto_reset, the header survives both
RUN_ALLOCATOR_TESTS :: true; // the allocator proc's modes, get_allocator, get_capabilities
VERBOSE :: false; // print cursor positions as groups run
// ---------------------------------------------------------------------------
// Harness
// ---------------------------------------------------------------------------
// Per-group tallies. Fixed size and allocation-free on purpose: expect() is called from
// inside push_arena scopes, so anything it allocated would come out of the arena under
// test and be freed the moment that arena is reset or deleted.
MAX_GROUPS :: 16;
group_names : [MAX_GROUPS] string;
group_checks : [MAX_GROUPS] int;
group_fails : [MAX_GROUPS] int;
group_count := 0;
checks_run := 0;
checks_failed := 0;
// Set by run_fixed_arena_tests. One suite is one work item, so a file-scope pointer is
// enough; null means write straight to stdout, as a synchronous or standalone run does.
output_builder: *String_Builder;
emit :: (format_string: string, args: ..Any) {
if output_builder print_to_builder(output_builder, format_string, ..args);
else print(format_string, ..args);
} @PrintLike
group :: (name: string) {
assert(group_count < MAX_GROUPS, "raise MAX_GROUPS");
group_names[group_count] = name;
group_count += 1;
emit("\n-- % --\n", name);
}
expect :: (condition: bool, format: string, args: ..Any, loc := #caller_location) {
checks_run += 1;
if group_count group_checks[group_count-1] += 1;
if condition return;
checks_failed += 1;
if group_count group_fails[group_count-1] += 1;
name := ifx group_count then group_names[group_count-1] else "?";
emit(" FAIL [%:%] %\n", name, loc.line_number, tprint(format, ..args));
}
expecting_errors :: (what: string) {
emit(" (expecting logged errors: %)\n", what);
}
is_aligned :: (pointer: *void, alignment: s64) -> bool {
return (cast(s64) pointer) % alignment == 0;
}
fill :: (memory: *void, count: s64, value: u8) {
bytes := cast(*u8) memory;
for 0..count-1 bytes[it] = value;
}
all_bytes_are :: (memory: *void, count: s64, value: u8) -> bool {
bytes := cast(*u8) memory;
for 0..count-1 if bytes[it] != value return false;
return true;
}
// How much of a block of `size` bytes is actually available to hand out, ignoring the
// alignment padding between allocations.
usable_bytes :: (size: s64) -> s64 {
return size - FIXED_ARENA_HEADER_BYTES;
}
TEST_SIZE :: 4096;
// ---------------------------------------------------------------------------
// Lifecycle: bootstrap_fixed_arena, fixed_arena_init, delete_fixed_arena, is_valid
// ---------------------------------------------------------------------------
lifecycle_tests :: () {
group("lifecycle");
// A zeroed FixedArena is not valid until it is initialized. Regression guard for M4:
// the old is_valid tested `count >= 0`, which holds for every well-formed slice.
{
a: FixedArena;
expect(!is_valid(*a), "a zeroed FixedArena reported valid");
}
// Bootstrapping establishes every invariant the rest of the file depends on.
{
arena := bootstrap_fixed_arena(TEST_SIZE);
expect(arena != null, "bootstrap_fixed_arena(%) returned null", TEST_SIZE);
if arena {
expect(is_valid(arena), "a freshly bootstrapped arena is not valid");
expect(arena.memory.count == TEST_SIZE, "block is % bytes, expected %", arena.memory.count, TEST_SIZE);
expect(arena.cursor == FIXED_ARENA_HEADER_BYTES,
"cursor starts at %, expected % so the header is not handed out", arena.cursor, FIXED_ARENA_HEADER_BYTES);
// The header lives at the base of the block it describes.
expect(cast(*void)arena == cast(*void)arena.memory.data,
"the bootstrapped header is not at the base of its own block");
expect(fixed_arena_start(arena) == arena.memory.data + FIXED_ARENA_HEADER_BYTES,
"fixed_arena_start does not skip the header");
expect(arena.allocator.proc != null, "bootstrap did not record the allocator that made the block");
delete_fixed_arena(arena);
// `arena` is dangling from here; do not read it. See the comment on
// bootstrap_fixed_arena.
}
}
// Sizes below the header are refused rather than writing the header past the end of the
// allocation. Regression guard for C3.
{
expecting_errors("bootstrap with sizes below the minimum");
expect(bootstrap_fixed_arena(0) == null, "bootstrap_fixed_arena(0) returned an arena");
expect(bootstrap_fixed_arena(-1) == null, "bootstrap_fixed_arena(-1) returned an arena");
expect(bootstrap_fixed_arena(FIXED_ARENA_MINIMUM_SIZE - 1) == null,
"bootstrap accepted % bytes, one below the minimum of %",
FIXED_ARENA_MINIMUM_SIZE - 1, FIXED_ARENA_MINIMUM_SIZE);
}
// The minimum itself is accepted, so the boundary is where it claims to be.
{
arena := bootstrap_fixed_arena(FIXED_ARENA_MINIMUM_SIZE);
expect(arena != null, "bootstrap refused exactly FIXED_ARENA_MINIMUM_SIZE (%)", FIXED_ARENA_MINIMUM_SIZE);
if arena delete_fixed_arena(arena);
}
// fixed_arena_init wraps memory the caller owns, with the struct outside the block.
// Regression guard for L3.
{
buffer: [TEST_SIZE] u8;
a: FixedArena;
ok := fixed_arena_init(*a, buffer);
expect(ok, "fixed_arena_init over a % byte buffer failed", TEST_SIZE);
expect(is_valid(*a), "an arena initialized over a caller buffer is not valid");
expect(a.memory.data == buffer.data, "fixed_arena_init did not record the caller's block");
expect(a.cursor == FIXED_ARENA_HEADER_BYTES,
"fixed_arena_init starts at %, expected % -- both layouts share one address map",
a.cursor, FIXED_ARENA_HEADER_BYTES);
expect(a.allocator.proc == null, "fixed_arena_init invented an allocator for caller-owned memory");
// Deleting it must not try to free the caller's buffer, and must leave the struct
// usable, unlike the bootstrapped case.
delete_fixed_arena(*a);
expect(is_valid(*a), "delete_fixed_arena damaged an arena over caller-owned memory");
}
// Bad arguments to fixed_arena_init are refused.
{
expecting_errors("fixed_arena_init with a null arena and an undersized block");
small: [8] u8;
a: FixedArena;
expect(!fixed_arena_init(null, small), "fixed_arena_init(null, ...) reported success");
expect(!fixed_arena_init(*a, small),
"fixed_arena_init accepted an 8 byte block, below the minimum of %", FIXED_ARENA_MINIMUM_SIZE);
expect(!is_valid(*a), "a failed fixed_arena_init left the arena looking valid");
}
// delete_fixed_arena tolerates null. Regression guard for H2.
{
delete_fixed_arena(null);
expect(true, "delete_fixed_arena(null) did not fault");
}
}
// ---------------------------------------------------------------------------
// Allocation: fixed_arena_alloc happy paths and cursor bookkeeping
// ---------------------------------------------------------------------------
allocation_tests :: () {
group("allocation");
arena := bootstrap_fixed_arena(TEST_SIZE);
if !arena {
expect(false, "bootstrap failed; skipping allocation tests");
return;
}
defer delete_fixed_arena(arena);
// A plain allocation lands inside the block, past the header.
{
p := fixed_arena_alloc(arena, 64);
expect(p != null, "a 64 byte allocation from a % byte arena returned null", TEST_SIZE);
expect(p >= fixed_arena_start(arena), "an allocation landed inside the header region");
expect(p < cast(*void)(arena.memory.data + arena.memory.count), "an allocation landed past the end of the block");
// The whole request is writable. This is the check that would have caught C1 in a
// build with array bounds checking disabled.
fill(p, 64, 0xAB);
expect(all_bytes_are(p, 64, 0xAB), "the allocated range did not survive being written");
}
// Successive allocations do not overlap.
{
fixed_arena_reset(arena);
a := fixed_arena_alloc(arena, 100);
b := fixed_arena_alloc(arena, 100);
expect(a != null && b != null, "allocations failed early in a % byte arena", TEST_SIZE);
if a && b {
fill(a, 100, 0x11);
fill(b, 100, 0x22);
expect(all_bytes_are(a, 100, 0x11), "the second allocation overlapped the first");
expect(cast(*u8)b >= cast(*u8)a + 100, "the second allocation starts inside the first");
}
}
// A zero-byte allocation is legal and does not move the cursor other than by alignment.
{
fixed_arena_reset(arena);
before := arena.cursor;
p := fixed_arena_alloc(arena, 0);
expect(p != null, "a zero-byte allocation returned null");
expect(arena.cursor >= before, "a zero-byte allocation moved the cursor backwards");
expect(arena.cursor - before < FIXED_ARENA_DEFAULT_ALIGNMENT,
"a zero-byte allocation consumed % bytes, more than alignment padding", arena.cursor - before);
}
// The cursor tracks what was handed out.
{
fixed_arena_reset(arena);
p := fixed_arena_alloc(arena, 200);
expect(p != null, "a 200 byte allocation returned null");
if p {
end := cast(s64)(cast(*u8)p - arena.memory.data) + 200;
expect(arena.cursor == end, "cursor is % after a 200 byte allocation ending at %", arena.cursor, end);
}
}
#if VERBOSE emit(" cursor after allocation tests: %\n", arena.cursor);
}
// ---------------------------------------------------------------------------
// Bounds: exhaustion, exact capacity, negative and overflowing sizes
// ---------------------------------------------------------------------------
bounds_tests :: () {
group("bounds");
arena := bootstrap_fixed_arena(TEST_SIZE);
if !arena {
expect(false, "bootstrap failed; skipping bounds tests");
return;
}
defer delete_fixed_arena(arena);
// Allocating past capacity returns null and leaves the cursor where it was. Regression
// guard for C1, and for H5 on the cursor half.
{
expecting_errors("an allocation larger than the block");
fixed_arena_reset(arena);
before := arena.cursor;
p := fixed_arena_alloc(arena, TEST_SIZE * 2);
expect(p == null, "an allocation twice the size of the block succeeded");
expect(arena.cursor == before,
"a rejected allocation moved the cursor from % to % -- alignment padding was consumed on failure",
before, arena.cursor);
}
// The block can be filled exactly, and the next byte fails. Regression guard for C1's
// off-by-one at exact capacity: the old code indexed arena.memory[cursor], which is out
// of bounds when cursor == count even for a zero-byte request.
{
expecting_errors("one byte past a full block");
fixed_arena_reset(arena);
// Fill to the end. Ask fixed_arena_available_bytes rather than computing
// `memory.count - cursor`: the next allocation starts at the next aligned address, and
// the padding to reach it comes out of the block too.
first := fixed_arena_alloc(arena, 1);
expect(first != null, "a one byte allocation failed on an empty arena");
remaining := fixed_arena_available_bytes(arena);
expect(remaining < arena.memory.count - arena.cursor + 1,
"fixed_arena_available_bytes (%) exceeds the raw gap to the end of the block (%)",
remaining, arena.memory.count - arena.cursor);
rest := fixed_arena_alloc(arena, remaining);
expect(rest != null, "could not allocate the remaining % bytes to fill the block exactly", remaining);
expect(arena.cursor == arena.memory.count,
"after filling exactly, cursor is % and capacity is %", arena.cursor, arena.memory.count);
// A zero-byte request against a full arena is still legal: no memory is handed out.
zero := fixed_arena_alloc(arena, 0);
expect(zero != null, "a zero-byte allocation against an exactly full arena was refused");
// One real byte is not.
one := fixed_arena_alloc(arena, 1);
expect(one == null, "allocated a byte past the end of a full block");
// And a full arena reports nothing available.
expect(fixed_arena_available_bytes(arena) == 0,
"a full arena reports % bytes available", fixed_arena_available_bytes(arena));
}
// fixed_arena_available_bytes is exactly the largest request that can succeed -- not an estimate,
// and not the raw gap `memory.count - cursor`, which overstates it by the alignment padding.
// This is the invariant the suite's first run got wrong at three separate sites.
{
expecting_errors("one byte more than fixed_arena_available_bytes reports");
// Start from an odd cursor so there is real padding to account for.
fixed_arena_reset(arena);
fixed_arena_alloc(arena, 3);
available := fixed_arena_available_bytes(arena);
expect(available > 0, "an arena with one 3 byte allocation reports nothing available");
// One more than it reports must fail, and must not move the cursor.
before := arena.cursor;
expect(fixed_arena_alloc(arena, available + 1) == null,
"allocating fixed_arena_available_bytes + 1 (%) succeeded", available + 1);
expect(arena.cursor == before, "a rejected over-request moved the cursor to %", arena.cursor);
// Exactly what it reports must succeed, and must land on the end of the block.
p := fixed_arena_alloc(arena, available);
expect(p != null, "allocating exactly fixed_arena_available_bytes (%) failed", available);
expect(arena.cursor == arena.memory.count,
"allocating everything available left the cursor at % of %", arena.cursor, arena.memory.count);
}
// A zero-byte request must not depend on where the block happens to start. On a block whose
// base is not FIXED_ARENA_DEFAULT_ALIGNMENT-aligned, aligning at exact capacity walks past
// the end -- so without the zero-byte early return this is refused, and the guarantee in
// docs/fixed-arena.md is false for exactly the buffers fixed_arena_init exists to wrap.
// A bootstrapped block comes from the heap already aligned, so it cannot reach this case.
{
backing: [512] u8;
base := backing.data;
while (cast(s64)base) % FIXED_ARENA_DEFAULT_ALIGNMENT != 8 {
base += 1;
}
// A multiple of the alignment, so that the END of the block is misaligned too. That is
// what makes this case discriminating: if the end happened to be aligned, aligning at
// exact capacity would land on it and a zero-byte request would succeed either way.
BLOCK_BYTES :: 256;
block: []u8;
block.data = base;
block.count = BLOCK_BYTES;
misaligned: FixedArena;
ok := fixed_arena_init(*misaligned, block);
expect(ok, "could not initialize an arena over a misaligned block");
if ok {
expect((cast(s64)misaligned.memory.data) % FIXED_ARENA_DEFAULT_ALIGNMENT != 0,
"the test block is aligned after all; this case proves nothing");
expect((cast(s64)(misaligned.memory.data + misaligned.memory.count)) % FIXED_ARENA_DEFAULT_ALIGNMENT != 0,
"the test block ends on an aligned address; this case proves nothing");
// Fill it exactly.
fill_size := fixed_arena_available_bytes(*misaligned);
expect(fixed_arena_alloc(*misaligned, fill_size) != null,
"could not fill the misaligned block with its reported % bytes", fill_size);
expect(misaligned.cursor == misaligned.memory.count,
"the misaligned block is not exactly full: cursor % of %",
misaligned.cursor, misaligned.memory.count);
expect(fixed_arena_alloc(*misaligned, 0) != null,
"a zero-byte allocation was refused by a full block based at %, which makes the result depend on the base address",
misaligned.memory.data);
}
}
// Negative sizes are refused rather than walking the cursor backwards. Regression guard
// for C2 -- the old deferred assert was `cursor <= count`, which passes on a negative
// cursor because it guards the wrong end.
{
expecting_errors("negative allocation sizes");
fixed_arena_reset(arena);
before := arena.cursor;
expect(fixed_arena_alloc(arena, -1) == null, "a -1 byte allocation returned a pointer");
expect(fixed_arena_alloc(arena, -TEST_SIZE) == null, "a large negative allocation returned a pointer");
expect(arena.cursor == before, "a negative allocation moved the cursor to %", arena.cursor);
expect(arena.cursor >= 0, "the cursor went negative");
}
// A size near S64_MAX must fail the capacity check rather than wrap past it. Regression
// guard for C2's overflow half: written as `byte_count > capacity - aligned`, this is
// safe; written as `aligned + byte_count > capacity` it would wrap negative and pass.
{
expecting_errors("an allocation near S64_MAX");
fixed_arena_reset(arena);
before := arena.cursor;
huge := 0x7FFF_FFFF_FFFF_FFFF;
expect(fixed_arena_alloc(arena, huge) == null, "an allocation of S64_MAX bytes succeeded");
expect(arena.cursor == before, "an overflowing allocation moved the cursor to %", arena.cursor);
expect(is_valid(arena), "the arena did not survive an overflowing request");
}
// A null or invalid arena is declined, not asserted on. Regression guard for M5.
{
expecting_errors("allocation against a null and an invalid arena");
expect(fixed_arena_alloc(null, 64) == null, "allocating from a null arena returned a pointer");
broken: FixedArena; // zeroed: no block at all
expect(fixed_arena_alloc(*broken, 64) == null, "allocating from an uninitialized arena returned a pointer");
}
}
// ---------------------------------------------------------------------------
// Alignment: results aligned as addresses, not as offsets
// ---------------------------------------------------------------------------
alignment_tests :: () {
group("alignment");
// Every result out of a bootstrapped arena is aligned.
{
arena := bootstrap_fixed_arena(TEST_SIZE);
if !arena {
expect(false, "bootstrap failed; skipping alignment tests");
return;
}
defer delete_fixed_arena(arena);
// Odd sizes so the cursor lands off-alignment between allocations.
sizes := s64.[1, 3, 7, 17, 33, 65, 129];
for sizes {
p := fixed_arena_alloc(arena, it);
expect(p != null, "a % byte allocation failed", it);
if p {
expect(is_aligned(p, FIXED_ARENA_DEFAULT_ALIGNMENT),
"a % byte allocation returned %, which is not %-byte aligned", it, p, FIXED_ARENA_DEFAULT_ALIGNMENT);
}
}
}
// The one that matters: a block whose base is NOT 16-aligned. Aligning the offset rather
// than the address would return misaligned pointers here while looking correct above.
// Regression guard for H1 -- an 8-aligned block is exactly what an Arena hands out,
// since ARENA_DEFAULT_ALIGNMENT is 8.
{
backing: [TEST_SIZE] u8;
// Walk the base forward to the next address that is 8-aligned but not 16-aligned.
base := backing.data;
while (cast(s64)base) % 16 != 8 {
base += 1;
}
available := TEST_SIZE - cast(s64)(base - backing.data);
misaligned_block: []u8;
misaligned_block.data = base;
misaligned_block.count = available;
a: FixedArena;
ok := fixed_arena_init(*a, misaligned_block);
expect(ok, "could not initialize an arena over a deliberately misaligned block");
expect((cast(s64)a.memory.data) % FIXED_ARENA_DEFAULT_ALIGNMENT != 0,
"the test block is already aligned; this case proves nothing");
if ok {
for 1..8 {
p := fixed_arena_alloc(*a, it * 5);
expect(p != null, "allocation % failed on the misaligned block", it);
if p {
expect(is_aligned(p, FIXED_ARENA_DEFAULT_ALIGNMENT),
"allocation % returned %, not %-byte aligned, from a block based at %",
it, p, FIXED_ARENA_DEFAULT_ALIGNMENT, a.memory.data);
}
}
}
}
}
// ---------------------------------------------------------------------------
// Resize: in-place growth and shrink, copy fallback, null old_memory
// ---------------------------------------------------------------------------
resize_tests :: () {
group("resize");
arena := bootstrap_fixed_arena(TEST_SIZE);
if !arena {
expect(false, "bootstrap failed; skipping resize tests");
return;
}
defer delete_fixed_arena(arena);
// Growing the most recent allocation extends it where it sits. Regression guard for M6:
// without this, growing an array to N elements costs O(N^2) bytes of a block that
// cannot grow.
{
fixed_arena_reset(arena);
p := fixed_arena_alloc(arena, 64);
expect(p != null, "the initial 64 byte allocation failed");
if p {
fill(p, 64, 0x5A);
grown := fixed_arena_resize(arena, p, 64, 128);
expect(grown == p, "growing the most recent allocation moved it from % to %", p, grown);
expect(all_bytes_are(grown, 64, 0x5A), "growing in place did not preserve the original bytes");
expect(arena.cursor == cast(s64)(cast(*u8)p - arena.memory.data) + 128,
"cursor is % after growing to 128 bytes in place", arena.cursor);
}
}
// Shrinking the most recent allocation reclaims the difference.
{
fixed_arena_reset(arena);
p := fixed_arena_alloc(arena, 256);
expect(p != null, "the initial 256 byte allocation failed");
if p {
shrunk := fixed_arena_resize(arena, p, 256, 64);
expect(shrunk == p, "shrinking the most recent allocation moved it");
expect(arena.cursor == cast(s64)(cast(*u8)p - arena.memory.data) + 64,
"shrinking did not reclaim: cursor is %", arena.cursor);
}
}
// Resizing anything other than the most recent allocation copies.
{
fixed_arena_reset(arena);
first := fixed_arena_alloc(arena, 64);
second := fixed_arena_alloc(arena, 64);
expect(first != null && second != null, "setup allocations failed");
if first && second {
fill(first, 64, 0x77);
moved := fixed_arena_resize(arena, first, 64, 128);
expect(moved != null, "resizing a buried allocation returned null");
expect(moved != first, "a buried allocation was extended in place, over its neighbour");
expect(all_bytes_are(moved, 64, 0x77), "the copy fallback did not preserve the original bytes");
}
}
// A resize with no previous allocation behaves as an allocation. Regression guard for
// H3: the memcpy used to run unconditionally.
{
fixed_arena_reset(arena);
p := fixed_arena_resize(arena, null, 0, 64);
expect(p != null, "resize from null/0 returned null");
// And a non-zero old_size with a null pointer must not be read from.
q := fixed_arena_resize(arena, null, 32, 64);
expect(q != null, "resize from null with a stale old_size returned null");
}
// A resize that cannot fit returns null rather than a short block.
{
expecting_errors("a resize larger than the block");
fixed_arena_reset(arena);
p := fixed_arena_alloc(arena, 64);
expect(p != null, "the initial allocation failed");
if p {
expect(fixed_arena_resize(arena, p, 64, TEST_SIZE * 2) == null,
"a resize to twice the block size succeeded");
}
}
// Negative and invalid inputs are declined.
{
expecting_errors("negative resize and a null arena");
expect(fixed_arena_resize(arena, null, 0, -1) == null, "a negative resize returned a pointer");
expect(fixed_arena_resize(null, null, 0, 64) == null, "resizing against a null arena returned a pointer");
}
}
// ---------------------------------------------------------------------------
// Reset: fixed_arena_reset, auto_reset, and the header surviving both
// ---------------------------------------------------------------------------
reset_tests :: () {
group("reset");
arena := bootstrap_fixed_arena(TEST_SIZE);
if !arena {
expect(false, "bootstrap failed; skipping reset tests");
return;
}
defer delete_fixed_arena(arena);
// Reset returns the cursor to the start of usable memory, NOT to zero. Regression guard
// for M1: resetting to zero would let the next allocation overwrite the header.
{
p := fixed_arena_alloc(arena, 512);
expect(p != null, "the setup allocation failed");
fixed_arena_reset(arena);
expect(arena.cursor == FIXED_ARENA_HEADER_BYTES,
"reset put the cursor at %, expected % -- resetting to 0 would hand out the header",
arena.cursor, FIXED_ARENA_HEADER_BYTES);
// The arena survives being reused, which is what proves the header was not clobbered.
q := fixed_arena_alloc(arena, 512);
expect(q != null, "the first allocation after a reset failed");
expect(q == p, "reset did not reuse the block: % then %", p, q);
expect(is_valid(arena), "the arena is no longer valid after a reset-and-reallocate cycle");
expect(arena.memory.count == TEST_SIZE, "the block size was corrupted by a reset cycle: %", arena.memory.count);
}
// The whole block is reusable after a reset, repeatedly.
{
for 1..3 {
fixed_arena_reset(arena);
remaining := fixed_arena_available_bytes(arena);
p := fixed_arena_alloc(arena, remaining);
expect(p != null, "could not refill the block on pass %", it);
}
fixed_arena_reset(arena);
}
// auto_reset restores the cursor at scope exit. Regression guard for M1's other half:
// push_arena alone restores context.allocator, not the cursor.
{
fixed_arena_reset(arena);
outer := arena.cursor;
{
auto_reset(arena);
p := fixed_arena_alloc(arena, 256);
expect(p != null, "the allocation inside an auto_reset scope failed");
expect(arena.cursor > outer, "the cursor did not move inside the scope");
}
expect(arena.cursor == outer, "auto_reset left the cursor at %, expected %", arena.cursor, outer);
}
// Nested auto_reset scopes unwind in order.
{
fixed_arena_reset(arena);
level0 := arena.cursor;
{
auto_reset(arena);
fixed_arena_alloc(arena, 64);
level1 := arena.cursor;
{
auto_reset(arena);
fixed_arena_alloc(arena, 64);
expect(arena.cursor > level1, "the inner scope did not allocate");
}
expect(arena.cursor == level1, "the inner auto_reset restored to % instead of %", arena.cursor, level1);
}
expect(arena.cursor == level0, "the outer auto_reset restored to % instead of %", arena.cursor, level0);
}
// Reset declines an invalid arena rather than writing to it.
{
broken: FixedArena;
fixed_arena_reset(*broken);
expect(broken.cursor == 0, "fixed_arena_reset wrote to an uninitialized arena");
fixed_arena_reset(null);
expect(true, "fixed_arena_reset(null) did not fault");
}
}
// ---------------------------------------------------------------------------
// Allocator interface: the proc's modes, get_allocator, get_capabilities
// ---------------------------------------------------------------------------
allocator_tests :: () {
group("allocator");
arena := bootstrap_fixed_arena(TEST_SIZE);
if !arena {
expect(false, "bootstrap failed; skipping allocator tests");
return;
}
defer delete_fixed_arena(arena);
// push_arena routes context allocations into the arena.
{
fixed_arena_reset(arena);
before := arena.cursor;
{
push_arena(arena);
p := alloc(128);
expect(p != null, "alloc inside a push_arena scope returned null");
expect(arena.cursor > before, "alloc inside a push_arena scope did not come out of the arena");
}
expect(context.allocator.proc != fixed_arena_allocator_proc, "push_arena did not restore context.allocator");
}
// get_allocator produces the same thing push_arena installs. Regression guard for L2.
{
a := get_allocator(arena);
expect(a.proc == fixed_arena_allocator_proc, "get_allocator returned the wrong proc");
expect(a.data == cast(*void)arena, "get_allocator did not bind the arena");
}
// IS_THIS_YOURS answers for live memory only. Regression guard for H4.
{
fixed_arena_reset(arena);
live := fixed_arena_alloc(arena, 64);
expect(live != null, "the setup allocation failed");
expect(fixed_arena_allocator_proc(.IS_THIS_YOURS, 0, 0, live, arena) != null,
"IS_THIS_YOURS disowned a live allocation");
last_byte := cast(*u8)live + 63;
expect(fixed_arena_allocator_proc(.IS_THIS_YOURS, 0, 0, last_byte, arena) != null,
"IS_THIS_YOURS disowned the last live byte");
// The unallocated tail is not ours: nobody was given it.
tail := arena.memory.data + arena.cursor;
expect(fixed_arena_allocator_proc(.IS_THIS_YOURS, 0, 0, tail, arena) == null,
"IS_THIS_YOURS claimed the unallocated tail");
// Neither is the header, which was never handed out.
expect(fixed_arena_allocator_proc(.IS_THIS_YOURS, 0, 0, arena.memory.data, arena) == null,
"IS_THIS_YOURS claimed the header region");
stack_value := 0;
expect(fixed_arena_allocator_proc(.IS_THIS_YOURS, 0, 0, *stack_value, arena) == null,
"IS_THIS_YOURS claimed a stack address");
// A reset un-owns everything it previously handed out.
fixed_arena_reset(arena);
expect(fixed_arena_allocator_proc(.IS_THIS_YOURS, 0, 0, live, arena) == null,
"IS_THIS_YOURS still claims memory released by a reset");
}
// CAPS advertises what the proc actually implements. Regression guard for M2.
{
caps, name := get_capabilities(get_allocator(arena));
expect(caps & .IS_THIS_YOURS == .IS_THIS_YOURS, "CAPS does not advertise IS_THIS_YOURS, but the mode is implemented");
expect(caps & .HINT_I_AM_A_FAST_BUMP_ALLOCATOR == .HINT_I_AM_A_FAST_BUMP_ALLOCATOR,
"CAPS does not advertise the bump-allocator hint");
expect(caps & .FREE == 0, "CAPS advertises FREE, but .FREE is a no-op");
expect(caps & .MULTIPLE_THREADS == 0, "CAPS advertises MULTIPLE_THREADS; nothing here is synchronized");
expect(name.count > 0, "CAPS did not fill in the allocator name, which callers may treat as a hard error");
}
// A null allocator_data must be survivable in every mode, because get_capabilities
// passes null outright: capabilities are a property of the proc, not of any one heap.
// Regression guard for M5.
{
expecting_errors("allocator calls with a null arena");
name: string;
caps := cast,no_check(Allocator_Caps) fixed_arena_allocator_proc(.CAPS, 0, 0, *name, null);
expect(caps & .IS_THIS_YOURS == .IS_THIS_YOURS, "CAPS with a null arena lost the IS_THIS_YOURS bit");
expect(name.count > 0, "CAPS with a null arena did not fill in the allocator name");
stack_value := 0;
expect(fixed_arena_allocator_proc(.IS_THIS_YOURS, 0, 0, *stack_value, null) == null,
"IS_THIS_YOURS claimed memory while holding a null arena");
expect(fixed_arena_allocator_proc(.ALLOCATE, 64, 0, null, null) == null,
"ALLOCATE with a null arena returned a pointer");
expect(fixed_arena_allocator_proc(.RESIZE, 64, 0, null, null) == null,
"RESIZE with a null arena returned a pointer");
}
// FREE is a no-op that returns null rather than falling off the end of the switch.
// Regression guard for M3.
{
fixed_arena_reset(arena);
p := fixed_arena_alloc(arena, 64);
before := arena.cursor;
expect(fixed_arena_allocator_proc(.FREE, 0, 64, p, arena) == null, "FREE returned a pointer");
expect(arena.cursor == before, "FREE moved the cursor");
expect(is_valid(arena), "the arena did not survive a FREE");
}
// The lifecycle modes are answerable and harmless.
{
expect(fixed_arena_allocator_proc(.STARTUP, 0, 0, null, arena) == null, "STARTUP returned a pointer");
expect(fixed_arena_allocator_proc(.SHUTDOWN, 0, 0, null, arena) == null, "SHUTDOWN returned a pointer");
expect(fixed_arena_allocator_proc(.THREAD_START, 0, 0, null, arena) == null, "THREAD_START returned a pointer");
expect(fixed_arena_allocator_proc(.THREAD_STOP, 0, 0, null, arena) == null, "THREAD_STOP returned a pointer");
expect(fixed_arena_allocator_proc(.CREATE_HEAP, 0, 0, null, arena) == null, "CREATE_HEAP returned a pointer");
expect(fixed_arena_allocator_proc(.DESTROY_HEAP, 0, 0, null, arena) == null, "DESTROY_HEAP returned a pointer");
expect(is_valid(arena), "the arena did not survive the lifecycle modes");
}
// A full arena reached through the context returns null rather than corrupting memory.
// This is C1 seen from the outside: the path an ordinary caller actually takes.
{
expecting_errors("exhausting the arena through the allocator interface");
fixed_arena_reset(arena);
push_arena(arena);
// Drain it.
remaining := fixed_arena_available_bytes(arena);
bulk := alloc(remaining);
expect(bulk != null, "could not drain the arena through alloc");
expect(alloc(1) == null, "alloc returned a pointer from a full arena");
expect(is_valid(arena), "the arena is not valid after being exhausted");
expect(arena.cursor <= arena.memory.count,
"the cursor (%) escaped the block (%) -- this is the C1 corruption", arena.cursor, arena.memory.count);
}
}
// ---------------------------------------------------------------------------
// What the numbers mean, so nobody reads more into them than is there:
//
// A "check" is one expect() call reached at runtime, not one distinct property. A check
// inside a loop counts once per iteration, so a loop can contribute more checks than every
// other test in its group put together. The per-group breakdown below makes that visible
// rather than burying it in one headline figure.
//
// Breadth is better measured by the number of expect() call sites in this file, and better
// still by whether a real defect would trip one. Every finding in
// fixed-arena-code-review.html that has an observable behaviour has a call site here that
// would have failed before the fix.
report :: () -> bool {
emit("\n-- results by group --\n");
for 0..group_count-1 {
if group_fails[it] {
emit(" % -- % checks, % FAILED\n", group_names[it], group_checks[it], group_fails[it]);
} else {
emit(" % -- % checks\n", group_names[it], group_checks[it]);
}
}
emit("\n");
if checks_failed == 0 {
emit("ALL FIXED ARENA TESTS PASSED (% checks over % groups)\n", checks_run, group_count);
return true;
}
emit("FIXED ARENA TESTS FAILED (% of % checks over % groups)\n", checks_failed, checks_run, group_count);
return false;
}
#scope_export
// builder null writes to stdout as it goes; pass one to buffer the whole report so a
// caller running several suites at once can print each intact. See src/test/testing.jai.
run_fixed_arena_tests :: (builder: *String_Builder = null) -> bool {
output_builder = builder;
emit("=== fixed_arena.jai test suite ===\n");
emit("size_of(FixedArena) = %, header = % bytes, minimum block = %, alignment = %\n",
size_of(FixedArena), FIXED_ARENA_HEADER_BYTES, FIXED_ARENA_MINIMUM_SIZE, FIXED_ARENA_DEFAULT_ALIGNMENT);
#if RUN_LIFECYCLE_TESTS { lifecycle_tests(); }
#if RUN_ALLOCATION_TESTS { allocation_tests(); }
#if RUN_BOUNDS_TESTS { bounds_tests(); }
#if RUN_ALIGNMENT_TESTS { alignment_tests(); }
#if RUN_RESIZE_TESTS { resize_tests(); }
#if RUN_RESET_TESTS { reset_tests(); }
#if RUN_ALLOCATOR_TESTS { allocator_tests(); }
return report();
}

72
modules/Basic/timing.jai Normal file
View File

@ -0,0 +1,72 @@
#import "Hash_Table";
#import "Math";
// Each file supplies TIMING_SUPPORTED, timing_read_ticks, and timing_read_tick_frequency_hz.
#if OS == .MACOS {
#load "timing_macos.jai";
} else #if CPU == .X64 {
#load "timing_x64.jai";
} else #if CPU == .ARM64 {
#load "timing_arm64.jai";
} else {
#assert false "timing.jai: no timing implementation for this CPU.";
}
TIMING_ACTIVE :: ENABLE_TIMING && TIMING_SUPPORTED;
FF :: (val: float64, width:=3) -> FormatFloat #expand { return formatFloat(val, trailing_width = width, zero_removal=.NO); }
// Hz, not MHz: QueryPerformanceFrequency can be a fractional number of MHz (19.2 on some ARM64).
TICK_FREQUENCY_HZ: u64;
timing_set_tick_frequency :: () {
#if TIMING_ACTIVE {
TICK_FREQUENCY_HZ = timing_read_tick_frequency_hz();
if (TICK_FREQUENCY_HZ == 0) {
// On x64: no usable frequency leaf (AMD, pre-Skylake Intel), or a hypervisor reports 0 in them.
log("[timing] Tick frequency not detected, set to 3000 MHz\n");
TICK_FREQUENCY_HZ = 3_000_000_000;
} else log("[timing] Tick frequency set to % MHz\n", FF(cast(float64) TICK_FREQUENCY_HZ / 1_000_000, 3));
}
}
Timed_Block_Print :: (block_name:="") #expand {
#if TIMING_ACTIVE {
if TICK_FREQUENCY_HZ == 0 {
log("[Timed_Block_Print] TICK_FREQUENCY_HZ not set (see: timing_set_tick_frequency)!\n");
return;
}
start_tick := timing_read_ticks();
`defer {
elapsed_ticks := timing_read_ticks() - start_tick;
elapsed_ticks_f64 := cast(float64)elapsed_ticks;
elapsed_time_seconds := elapsed_ticks_f64 / cast(float64) TICK_FREQUENCY_HZ;
block_name_print := ifx block_name != "" then block_name else #procedure_name();
log("[%1]: %2 (% ticks)\n",
block_name_print,
format_time_seconds(elapsed_time_seconds),
format_cycles(elapsed_ticks));
}
}
}
// time_code :: (my_code: Code) #expand {
// #if ENABLE_TIMING {
// start_time := GetUnixTimestamp();
// #insert my_code;
// elapsed_time := GetUnixTimestamp() - start_time;
// print("[time_code snippet]: %1\n", format_time_seconds(elapsed_time));
// }
// }
// timing_data: Table(string, u64);
// Initialize_Timing_Table :: () {
// #if ENABLE_TIMING {
// init(*timing_data, 512);
// init(*timing_data_unix, 512);
// timing_data.allocator = context.default_allocator;
// timing_data_unix.allocator = context.default_allocator;
// }
// }

View File

@ -0,0 +1,59 @@
// ARM64 outside macOS, which uses timing_macos.jai. The compiler cannot emit ARM64 assembly yet,
// so there is no direct read of the generic timer (CNTVCT_EL0 / CNTFRQ_EL0); each OS's clock
// stands in for it.
#if OS == .WINDOWS {
// #TODO: Use the ARM64 performance counters directly once the compiler can emit ARM64 assembly.
TIMING_SUPPORTED :: true;
timing_read_ticks :: () -> u64 #expand {
counter: W.LARGE_INTEGER;
W.QueryPerformanceCounter(*counter);
return cast,no_check(u64) counter.QuadPart;
}
timing_read_tick_frequency_hz :: () -> u64 {
frequency: W.LARGE_INTEGER;
if W.QueryPerformanceFrequency(*frequency) == .FALSE return 0;
return cast,no_check(u64) frequency.QuadPart;
}
} else #if OS == .LINUX {
// Nanoseconds from CLOCK_MONOTONIC_RAW, which is unaffected by NTP slewing.
TIMING_SUPPORTED :: true;
timing_read_ticks :: () -> u64 #expand {
ts: P.timespec;
P.clock_gettime(.MONOTONIC_RAW, *ts);
return cast,no_check(u64) ts.tv_sec * 1_000_000_000 + cast,no_check(u64) ts.tv_nsec;
}
timing_read_tick_frequency_hz :: () -> u64 {
return 1_000_000_000;
}
} else {
// Stub. TIMING_SUPPORTED is false, so timing.jai compiles its timers to nothing here and these
// are never called.
TIMING_SUPPORTED :: false;
timing_read_ticks :: () -> u64 #expand {
return 0;
}
timing_read_tick_frequency_hz :: () -> u64 {
return 0;
}
}
#scope_file
#if OS == .WINDOWS W :: #import "Windows";
#if OS == .LINUX P :: #import "POSIX";

View File

@ -0,0 +1,29 @@
// Any CPU, its own clock is right under Rosetta, where RDTSC and CPUID are emulated.
// mach_absolute_time counts at the rate mach_timebase_info gives: 125/3 ns per tick on
// Apple Silicon, the 24 MHz ARM generic timer, and 1 ns per tick on x64, Rosetta included.
TIMING_SUPPORTED :: true;
timing_read_ticks :: () -> u64 #expand {
return mach_absolute_time();
}
timing_read_tick_frequency_hz :: () -> u64 {
info: Mach_Timebase_Info;
if mach_timebase_info(*info) != 0 || info.numer == 0 return 0;
// numer/denom converts ticks to nanoseconds, so ticks per second is 1e9 * denom / numer.
return 1_000_000_000 * cast(u64) info.denom / info.numer;
}
#scope_file
Mach_Timebase_Info :: struct {
numer: u32;
denom: u32;
}
mach_absolute_time :: () -> u64 #foreign timing_libsystem;
mach_timebase_info :: (info: *Mach_Timebase_Info) -> s32 #foreign timing_libsystem;
timing_libsystem :: #library,system "libSystem";

View File

@ -0,0 +1,83 @@
// RDTSC ticks, converted with the TSC frequency from CPUID leaf 0x15, or with the base frequency
// from leaf 0x16 where 0x15 is missing or incomplete.
TIMING_SUPPORTED :: true;
timing_read_ticks :: () -> u64 #expand {
return X64.rdtsc();
}
timing_read_tick_frequency_hz :: () -> u64 {
hz := timing_cpuid_tsc_frequency_hz();
if hz return hz;
timing_check_cpu_id_frequency_information();
return cast(u64) CPU_BASE_FREQUENCY * 1_000_000;
}
// All units in MHz
CPU_BASE_FREQUENCY: u32; // Useful for timing with RDTSC.
CPU_MAX_FREQUENCY: u32; // This is not reliable, will return different values depending on which CPU the thread is called on.
// e.g. on 12900K, we see 5200 on P cores and 3900 on some E-cores.
// In general this isn't useful since all measurements are relative to base frequency.
CPU_BUS_FREQUENCY: u32; // reference bus frequency
CPU_FREQ_RESERVED: u32; // not sure what this is for; most likely unused.
freq_calc_start_time : float64;
freq_calc_start_tick : u64;
// A leaf above the CPU's maximum returns the highest basic leaf's data rather than zeros, so every
// leaf read here is checked against this first. That also makes a vendor check unnecessary: a CPU
// without a leaf reports a lower maximum.
timing_cpuid_max_basic_leaf :: () -> u32 {
max_basic_leaf: u32 = ---;
#asm {
xor.d a: gpr === a, a;
xor.d c: gpr === c, c;
cpuid a, b:, c, d:;
mov.d max_basic_leaf, a;
}
return max_basic_leaf;
}
// Leaf 0x15: TSC Hz = crystal Hz (ECX) * EBX / EAX. 0 when the leaf is missing or leaves any of
// the three out; many CPUs (Skylake client among them) give the ratio but not the crystal.
timing_cpuid_tsc_frequency_hz :: () -> u64 {
if timing_cpuid_max_basic_leaf() < 0x15 return 0;
denominator: u32 = ---;
numerator: u32 = ---;
crystal_hz: u32 = ---;
#asm {
mov.d a: gpr === a, 0x15;
xor.d c: gpr === c, c;
cpuid a, b:, c, d:;
mov.d denominator, a;
mov.d numerator, b;
mov.d crystal_hz, c;
}
if !denominator || !numerator || !crystal_hz return 0;
return cast(u64) crystal_hz * numerator / denominator;
}
// Leaf 0x16 is Intel's (Skylake on). Below it the values stay 0.
timing_check_cpu_id_frequency_information :: () {
if timing_cpuid_max_basic_leaf() < 0x16 return;
base_frequency := *CPU_BASE_FREQUENCY;
max_freq := *CPU_MAX_FREQUENCY;
bus_freq := *CPU_BUS_FREQUENCY;
idk_freq := *CPU_FREQ_RESERVED;
#asm {
mov.d a: gpr === a, 0x16 ; // mov eax, 22 ; 0x16 is the frequency info
xor.d c: gpr === c, c ; // xor ecx, ecx
cpuid a, b:, c, d: ; // cpuid
mov.d [base_frequency], a; // eax
mov.d [max_freq], b; // ebx
mov.d [bus_freq], c; // ecx
mov.d [idk_freq], d; // edx
}
}
X64 :: #import "Machine_X64";

86
modules/Basic/windows.jai Normal file
View File

@ -0,0 +1,86 @@
sleep_milliseconds :: (milliseconds: s32) {
Sleep(milliseconds);
}
set_working_directory :: (s: string) -> bool {
wide := utf8_to_wide(s,, allocator=temp);
if !wide return false;
result := SetCurrentDirectoryW(wide);
return (result != 0);
}
get_working_directory :: () -> result: string /* Temporary Storage */ { // Returns the empty string on failure.
status := GetCurrentDirectoryW(0, null);
utf16_buf := cast(*u16) alloc(status * size_of(u16),, temp);
if !utf16_buf return "";
status = GetCurrentDirectoryW(status, utf16_buf);
if status > 0 {
return wide_to_utf8(utf16_buf,, allocator=temp);
} else {
log_error("Unable to get the current directory.");
return "";
}
}
exit :: (status: s32) {
FlushFileBuffers(windows_standard_output);
FlushFileBuffers(windows_standard_error);
ExitProcess(xx status);
}
#scope_file
// We redefine this stuff here, so that we can use this module without
// importing modules/Windows, because that is huge and gnarly.
// This is an experiment, though. Maybe it's not worth duplicating all this stuff,
// if people are going to import Windows anwyay. Hard to say.
// -jblow, 6 February 2020
kernel32 :: #library,system "kernel32";
BOOL :: enum s32 { FALSE :: 0; TRUE :: 1; }
Sleep :: (milliseconds: s32) -> void #foreign kernel32;
QueryPerformanceFrequency :: (result: *s64) -> s32 #foreign kernel32;
QueryPerformanceCounter :: (result: *s64) -> s32 #foreign kernel32;
SetCurrentDirectoryW :: (name: *u16) -> BOOL #foreign kernel32;
GetCurrentDirectoryW :: (nBufferLength: DWORD, lpBuffer: *u16) -> DWORD #foreign kernel32;
GetLocalTime :: (time: *SYSTEMTIME) -> void #foreign kernel32;
GetSystemTime :: (time : *SYSTEMTIME) -> void #foreign kernel32;
FlushFileBuffers :: (hFile: HANDLE) -> BOOL #foreign kernel32;
ExitProcess :: (exit_code: u32) -> void #foreign kernel32;
SYSTEMTIME :: struct {
wYear : u16;
wMonth : u16;
wDayOfWeek : u16;
wDay : u16;
wHour : u16;
wMinute : u16;
wSecond : u16;
uMilliseconds : u16;
}
HANDLE :: *void;
HWND :: HANDLE;
DWORD :: u32;
RECT :: struct {
left, top, right, bottom: s32;
}
POINT :: struct {
x, y: s32;
}
// STD_INPUT_HANDLE :: -10;
STD_OUTPUT_HANDLE :: -11;
// STD_ERROR_HANDLE :: -12;
#import "Windows_Utf8";

View File

@ -0,0 +1,167 @@
// .mcap: one capture in one file. A fixed magic, a file version stamp, the capture's Capture Text,
// a small preview, then the images. See docs/mcap.md for the layout.
//
// Reading gives views into the bytes it was given; nothing is copied. Images stay PNG.
MCAP_MAGIC :: u8.[0x89, #char "M", #char "C", #char "A", #char "P", 0x0D, 0x0A, 0x1A];
MCAP_VERSION :: 1;
MCAP_EXTENSION :: "mcap";
MCAP_PREVIEW_MAX_SIDE :: 320; // the preview's longest side, in pixels
Mcap_Rendered_Kind :: enum u8 {
NONE :: 0;
SAME_AS_ORIGINAL :: 1; // the rendered image is byte for byte the original, so it is stored once
OWN :: 2;
}
Mcap_Image :: struct {
width: u16;
height: u16;
png: [] u8; // empty when there is none
}
Mcap :: struct {
version: u32 = MCAP_VERSION; // as read; always MCAP_VERSION when written
text: string; // Capture Text
preview: Mcap_Image;
original_name: string; // the original image's name in the .snagx, such as {GUID}.png
original: [] u8; // PNG
rendered_kind: Mcap_Rendered_Kind;
rendered: [] u8; // PNG; OWN only, see mcap_rendered
}
// The rendered image (Snagit's thumbnail.png) whichever way it is stored; empty when there is none.
mcap_rendered :: (m: Mcap) -> [] u8 {
if m.rendered_kind == {
case .SAME_AS_ORIGINAL; return m.original;
case .OWN; return m.rendered;
}
return .[];
}
// ---------------------------------------------------------------- writing
// The file's bytes. A part too large for its size field fails the whole file rather than writing one
// that cannot be read back.
mcap_serialize :: (m: Mcap) -> Serializer, ok: bool, error: string {
size := MCAP_HEADER_SIZE + 4 + m.text.count + 8 + m.preview.png.count + 2 + m.original_name.count + 4 + m.original.count + 1;
if m.rendered_kind == .OWN size += 4 + m.rendered.count;
s := new_serializer(size);
magic := MCAP_MAGIC;
AddArray_NoSize(*s, magic);
Add(*s, cast(u32) MCAP_VERSION);
if !AddString(*s, m.text, u32) return s, false, "the text is too large for its size field";
Add(*s, m.preview.width);
Add(*s, m.preview.height);
if !AddSizedArray(*s, m.preview.png, u32) return s, false, "the preview is too large for its size field";
if !AddString(*s, m.original_name, u16) return s, false, "the original image's name is too long";
if !AddSizedArray(*s, m.original, u32) return s, false, "the original image is too large for its size field";
Add(*s, m.rendered_kind);
if m.rendered_kind == .OWN && !AddSizedArray(*s, m.rendered, u32) return s, false, "the rendered image is too large for its size field";
assert(s.count == size);
return s, true, "";
}
// Writes beside path and then swaps it in, so a failure leaves any earlier file as it was.
mcap_write :: (path: string, bytes: [] u8) -> ok: bool, error: string {
temp_path := tprint("%.tmp", path);
if !write_entire_file(temp_path, bytes.data, bytes.count) return false, tprint("cannot write %", temp_path);
if !atomic_file_replace_or_just_rename_if_destination_file_does_not_exist(temp_path, path) {
file_delete(temp_path);
return false, tprint("cannot move % to %", temp_path, path);
}
return true, "";
}
// ---------------------------------------------------------------- reading
// A whole .mcap. The parts are views into data. Exactly the file's bytes must be consumed.
mcap_parse :: (data: [] u8) -> Mcap, ok: bool, error: string {
m: Mcap;
d := to_deserializer(data);
ok, error := mcap_read_front_part(*d, *m);
if !ok return m, false, error;
m.original_name = ReadSizedString(*d, u16);
m.original = ReadSizedArray(*d, u8, u32);
m.rendered_kind = Read(*d, Mcap_Rendered_Kind);
if d.ok && m.rendered_kind == .OWN m.rendered = ReadSizedArray(*d, u8, u32);
if !d.ok return m, false, "the file ends early (it is truncated or damaged)";
if cast(u8) m.rendered_kind > cast(u8) Mcap_Rendered_Kind.OWN return m, false, tprint("the rendered image is of unknown kind %", cast(u8) m.rendered_kind);
if d.cursor != d.count return m, false, tprint("% bytes follow the last part", d.count - d.cursor);
return m, true, "";
}
// Only the header, text and preview, as a library browser needs them: one read of the start of the
// file, and at most two more when the text or preview runs past it. The images are left unread and
// empty. The parts are views into a buffer from the context allocator.
mcap_read_front :: (path: string) -> Mcap, ok: bool, error: string {
m: Mcap;
file, opened := file_open(path, log_errors = false);
if !opened return m, false, "cannot be opened";
defer file_close(*file);
length, length_ok := file_length(file);
if !length_ok return m, false, "cannot be read";
wanted := min(length, MCAP_FRONT_READ_SIZE);
for attempt: 1..3 {
buffer := NewArray(wanted, u8, initialized = false);
if !file_read(file, buffer.data, wanted) { array_free(buffer); return m, false, "cannot be read"; }
d := to_deserializer(buffer);
ok, error := mcap_read_front_part(*d, *m);
if ok return m, true, "";
if d.ok || wanted == length { array_free(buffer); return m, false, error; } // not a matter of too few bytes
// Short: the sizes read so far say how much is needed.
needed := mcap_front_size(buffer);
array_free(buffer);
if needed <= wanted || needed > length return m, false, "the file ends early (it is truncated or damaged)";
wanted = needed;
if !file_set_position(file, 0) return m, false, "cannot be read";
}
return m, false, "cannot be read";
}
#scope_module
MCAP_HEADER_SIZE :: 12; // magic and version
MCAP_FRONT_READ_SIZE :: 64 * 1024; // holds the header, text and preview of nearly every capture
// Header, text and preview.
mcap_read_front_part :: (d: *Deserializer, m: *Mcap) -> ok: bool, error: string {
magic := ReadArrayView(d, MCAP_MAGIC.count, u8);
if !d.ok return false, "is not an .mcap file (it is shorter than the magic)";
for MCAP_MAGIC if magic[it_index] != it return false, "is not an .mcap file (its magic is wrong)";
m.version = Read(d, u32);
if !d.ok return false, "the file ends early (it is truncated or damaged)";
if m.version == 0 return false, "has file version 0, which does not exist";
if m.version > MCAP_VERSION return false, tprint("is file version %, but this build reads up to %; it was written by a later version", m.version, MCAP_VERSION);
m.text = ReadSizedString(d, u32);
m.preview.width = Read(d, u16);
m.preview.height = Read(d, u16);
m.preview.png = ReadSizedArray(d, u8, u32);
if !d.ok return false, "the file ends early (it is truncated or damaged)";
return true, "";
}
// How many bytes the header, text and preview take, as far as the sizes in prefix tell: with only the
// text's size in it, enough to reach the preview's size; with that too, the whole front.
mcap_front_size :: (prefix: [] u8) -> s64 {
d := to_deserializer(prefix);
d.cursor = MCAP_HEADER_SIZE;
text_size := cast(s64) Read(*d, u32);
preview_header := MCAP_HEADER_SIZE + 4 + text_size + 4; // where the preview's size is
if !d.ok || preview_header + 4 > prefix.count return preview_header + 4;
d.cursor = preview_header;
return preview_header + 4 + cast(s64) Read(*d, u32);
}
#import "Basic";
#import "File";
#import "File_Utilities";
#import "Serialization";

File diff suppressed because it is too large Load Diff

576
modules/Hash_Table.jai Normal file
View File

@ -0,0 +1,576 @@
/*
A simple hash table.
Notes Last Updated: 11 May 2022
# Overview
This hash table stores all entries in a contiguous array, for good performance when
looking things up. (Some tables work by storing linked lists of entries, but this can
lead to more cache misses.)
When storing a value, we map its hash to a slot index; if that slot is free, we
put the key and value there, otherwise we just keep incrementing the slot index
by 1 until we find an empty slot. Because the table can never be full, we are
guaranteed to find a slot eventually.
When looking up a value, we perform this same process to find the correct slot.
We use hash values to indicate whether slots are empty or removed, because we can't
really know that based on keys or values, given that keys or values can be of arbitrary
type.
A hash of 0 means that slot is not used, so new values can be put there. A hash of 1
means that slot was once valid, but has since been removed. A hash of 2 or higher
(`FIRST_VALID_HASH`) means this is a currently live entry.
Whenever we hash a key, if the result is less than 2, we just add 2 to it
to put it into the valid range. This means that four values out of our 32-bit hash range
have a doubled probability of collision, but that is a small price to pay.
You can iterate over the table straightforwardly:
for table {
key, value := it_index, it;
}
*/
/*
By way of explaining why it's all good and okay for this hash table to be so simple,
we'll quote Charles Bloom, who said in 2017:
-----
The last time I looked at hashing, my main conclusions were :
1. A properly loaded (eg. not too full (eg. less than 70% or so)) hash
table satisfies almost all queries in the first slot.
(I think any statistics or testing of open-address hashing at more than 70%
full is not helpful.)
2. Because of #1, what you do after the first probe is almost
irrelevant. The only exception to this is if you have some bad degeneracy
in your hash function or data, in which case anything you can do to break
that degeneracy is a useful difference. The simplest (and therefore best)
seems to be quadratic probing.
3. Because of #1, storing your data compactly, like {Hash,Key,Data} is
best, because you always take exactly one cache miss per query. There are
some advantages to doing Hash-Hash-Hash, Key-Key-Key, but doing that
naively means a much higher constant # of cache misses. (I've seen a lot
of hash table implementations that use multiple arrays;
eg. khash and google's, and that's just totally wrong.)
4. Because of #1, any prefetching was a lose. After you compute the hash,
you need that first line immediately, so there's no time for a prefetch.
And then you need the second line so rarely that a prefetch doesn't help.
5. Because of #1, Cuckoo hashing is strictly worse, since it needs 2 cache
lines instead of 1.
6. Because of #1, the hash function is at least as important as the lookup
method. The hash function probably takes more clocks.
The only time to ever look at hash table speed is if you are memory
constrained. If you're not, then hey just make it bigger and it also gets
faster. So given that you are memory constrained, you always have to trade
off against things like making the key/data smaller or omitting one or two
of hash/key/data. eg. if hash/key/data can be 8 bytes or 12 bytes instead
of 16, you can run the table much less full in the same memory.
*/
/*
This hash table started life as some code in C by Sean Barrett, but it's
been changed so much that it's probably no longer recognizable.
For some evidence that this table is reasonably fast, even for large table sizes,
see jai/examples/hash_table_test.jai. We maybe should put that test here,
but I didn't want to bloat the file. There is also some additional
documentation there about how we handle collisions in this table,
and why.
*/
#module_parameters (COUNT_COLLISIONS := false)();
// A hash table that maps keys to values using 32 bit hashes.
Table :: struct (Key_Type: Type, Value_Type: Type,
given_hash_function: (Key_Type) -> u32 = null,
given_compare_function: (Key_Type, Key_Type) -> bool = null,
LOAD_FACTOR_PERCENT: u32 = 70,
REFILL_REMOVED := true
) {
// If you leave compare_function null, the table will use
// operator ==.
count: int; // The number of valid items in the table.
allocated: int; // The number of slots for which we have allocated memory.
slots_filled: int; // The number of slots that can't be used for new items (either currently valid items or items that were removed).
allocator: Allocator;
Entry :: struct {
hash: u32;
key: Key_Type;
value: Value_Type;
}
entries: [] Entry;
// We either use the hash function that was passed to us, or,
// if it was null, we use a default one.
#if given_hash_function {
hash_function :: given_hash_function;
} else {
hash_function :: (key: Key_Type) -> u32 { return Hash.get_hash(key); };
}
// Same situation with the compare function.
#if given_compare_function {
compare_function :: given_compare_function;
} else {
compare_function :: (a: Key_Type, b: Key_Type) -> bool { return a == b; };
}
#if COUNT_COLLISIONS {
add\_collisions: s64;
find_collisions: s64;
}
SIZE_MIN :: 32;
}
is_valid :: (table: *Table) -> bool {
if table == null return false;
if table.entries.data == null return false;
if table.allocated == 0 return false;
Basic.assert(table.given_hash_function != null);
Basic.assert(table.given_compare_function != null);
if table.given_hash_function == null
return false;
if table.given_compare_function == null
return false;
return true;
}
for_expansion :: (table: *$T/Table, body: Code, flags: For_Flags) #expand {
#assert(!(flags & .REVERSE)); // We don't handle the reverse flag.
for * entry, i: table.entries {
if entry.hash < FIRST_VALID_HASH continue;
#if flags & .POINTER {
`it := *entry.value;
} else {
`it := entry.value;
}
`it_index := entry.key;
#insert (remove={entry.hash=REMOVED_HASH; table.count-=1;}) body;
}
}
// Initialize a `Table` and allocates the given number of slots.
// You don't have to call init; if you don't, you'll get
// an initial table of size `SIZE_MIN`.
table_init :: (using table: *Table, slots_to_allocate: s64 = 0) #deprecated "Calling init() on a Hash_Table is no longer necessary (a default-initialized table is all you need)." {
_internal_resize_memory(table, slots_to_allocate);
}
// Frees the memory allocated by the table.
table_deinit :: (table: *Table) {
Basic.free(table.entries.data,, table.allocator);
}
//
// This reset keeps the current number of allocated slots;
// it just clears occupancy.
//
table_reset_keeping_memory :: (using table: *Table) {
count = 0;
slots_filled = 0;
for * entries it.hash = 0;
}
//
// This resets the entire table to its default state,
// but preserves the allocator.
//
table_reset :: (using table: *Table) {
// Resize the table to 0, freeing the memory, but leave the table
// in a usable state, and preserve the allocator.
old_allocator := allocator;
Basic.free(entries.data,, table.allocator);
table.* = {};
allocator = old_allocator;
}
//
// You can call table_resize to set the size of the table before using it,
// or even while using it. But you can't resize a table to a smaller size
// than the current count. You also can't go below SIZE_MIN, and as usual
// it will get rounded up to a power of two. So this is really just for
// general sizing purposes ("I want like a million-slot hash table to start").
//
table_resize :: (using table: *Table, expected_count: s64) {
// Make sure we can at least hold what we are currently holding.
expected_count = Basic.max(expected_count, table.count);
// Compute the full table size for this expected_count, including empty slots.
new_slots := (expected_count * 100 + 99) / LOAD_FACTOR_PERCENT;
_internal_resize_memory(table, new_slots);
}
//
// Walk_Table is a macro that factors out the details of traversing the hash table,
// so that they can be re-used by multiple routines without having to keep these details
// in sync. We expect the calling site to define:
//
// table: Table;
// key: table.Key_Type;
//
// We write back into the caller:
//
// hash: u32;
// index: u32;
// table_while_loop: iterator variable you can use to 'continue', etc.
// Walk_Table is intended for our own internal use. User-level traversal should
// probably use the for_expansion.
//
Walk_Table :: (code: Code) #expand {
mask := cast,trunc(u32)(`table.allocated - 1);
`hash := `table.hash_function(`key);
if hash < FIRST_VALID_HASH hash += FIRST_VALID_HASH;
`index := hash & mask;
// We use the 'triangular numbers' version of quadratic probing.
// It yields more collisions per insertion than double hashing
// (double hashing gives us 0.74, triangular numbers give us 0.84)
// but the collisions are cheaper because the initial probes
// are cache-coherent, unlike with double hashing. At large table sizes
// this ends up mattering substantially. (examples/hash_table_test.jai,
// at the 128 million entry table size, takes 15.5 seconds with triangular
// numbers, 17.8 seconds with double hashing.)
// Triangular numbers are guaranteed to hit every entry in the table:
// https://fgiesen.wordpress.com/2015/02/22/triangular-numbers-mod-2n/
probe_increment : u32 = 1;
// If table.entries[index] is zero, that slot is unoccupied, so we are done walking.
// If it's nonzero, we do whatever the caller wants to do (for example, check whether
// it is the entry we are looking for).
while `table_while_loop := `table.entries[index].hash {
#insert code; // Do whatever the caller wants to do for this slot index.
index = (index + probe_increment) & mask; // Since table.allocated is always power of two, the & wraps our index within the table. (Requires that probe_increment is not big enough to wrap index into negative numbers, which it won't be because it's u32 and index is s64.);
probe_increment += 1;
}
}
// table_ensure_space
//
// Call this if you want to be able to add 'items' number of things to the table without the table
// resizing in the middle. Useful in cases where you want to add multiple items to a table inside
// one block of code, while keeping pointers to those items, and wanting those pointers to remain valid.
table_ensure_space :: (using table: *Table, items: s64) {
if (slots_filled + items)*100 >= allocated*LOAD_FACTOR_PERCENT {
// IMPORTANT: We check slots_filled in the 'if' above, because we want to ask
// whether we can accomodate the new items in the current available space,
// including space occupied by tombstones. But when we compute new_allocated,
// we use table.count instead of .slots_filled, because the act of resizing the table
// will remove the tombstones.
new_allocated := ((count + items) * 100) / LOAD_FACTOR_PERCENT;
new_allocated = next_power_of_two(new_allocated);
expand(table, new_allocated);
}
}
// table_add
//
// Adds the given key value pair to the table, returns a pointer to the inserted value.
// If you add a key twice, the table will not currently notice that this has happened,
// so you'll just get the first one. It's unclear whether we should assert on this;
// it sort of depends on how slow you think key comparison might be. Maybe that should
// be a compile-time parameter to the hash table? -jblow, 8 September 2020
table_add :: (table: *Table, key: table.Key_Type, value: table.Value_Type) -> *table.Value_Type {
// The + 1 is here to handle the weird case when the table size is 1 and you add the first item...
// If we just do table_count * 2 >= table.allocated, we would fill the table, causing an infinite loop on find.
#assert table.LOAD_FACTOR_PERCENT < 100; // A 100% full table will infinite loop (and you will want to be substantially smaller than this for reasonable performance).
// Without dividing, we want to test:
// (filled / allocated >= 70/100)
// Therefore, we say
// (filled * 100 >= allocated * 70)
if (table.slots_filled + 1)*100 >= table.allocated*table.LOAD_FACTOR_PERCENT expand(table);
Basic.assert(table.slots_filled < table.allocated);
Walk_Table(#code {
#if table.REFILL_REMOVED {
if table.entries[index].hash == REMOVED_HASH {
table.slots_filled -= 1; // 1 will get re-added below, for total increment 0.
break;
}
}
#if COUNT_COLLISIONS table.add_collisions += 1;
});
// Walk_Table walked us to an unused entry, so, add our new data into this slot.
table.count += 1;
table.slots_filled += 1;
entry := *table.entries[index];
entry.hash = hash;
entry.key = key;
entry.value = value;
return *entry.value;
}
// table_set
//
// Adds or replaces the given key value pair.
table_set :: (table: *Table, key: table.Key_Type, value: table.Value_Type) -> *table.Value_Type {
value_ptr := table_find_pointer(table, key);
if value_ptr {
value_ptr.* = value;
return value_ptr;
} else {
return table_add(table, key, value);
}
}
// table_contains
//
// Returns whether a table contains key. Useful when table is being used as a set.
table_contains :: (table: *Table, key: table.Key_Type) -> bool {
return table_find_pointer(table, key) != null;
}
// Lookup the given key and return a pointer to the corresponding value.
// If multiple values are added with the same key, we return the first match.
// If you think you might have key collisions, use table_find_multiple.
// We now take 'table' by pointer so that if COUNT_COLLISIONS, we can modify
// the collision count, which is stored on the table. If we stored it elsewhere
// this would no longer be necessary (but might be slower?) - 8 July 2022
table_find_pointer :: (table: *Table, key: table.Key_Type) -> *table.Value_Type {
if !table.allocated return null;
Walk_Table(#code {
entry := *table.entries[index];
if entry.hash == hash {
if inline table.compare_function(entry.key, key) return *entry.value;
}
#if COUNT_COLLISIONS table.find_collisions += 1;
});
return null;
}
// Lookup the given key and return the corresponding value.
//
// You need to pay attention to 'success' because if it's false,
// the return value will be uninitialized.
//
// See the note on table_find_pointer about collisions and about
// why 'table' is being passed by pointer.
table_find :: (table: *Table, key: table.Key_Type) -> (success: bool, table.Value_Type) {
pointer := inline table_find_pointer(table, key);
if pointer return true, pointer.*;
dummy: table.Value_Type;
return false, dummy;
}
// Warning: table_find_multiple has not yet been tested.
table_find_multiple :: (table: *Table, key: table.Key_Type) -> [] table.Value_Type /* Allocated with the temporary allocator! */ {
if !table.allocated return .[];
results: [..] table.Value_Type;
results.allocator = Basic.temporary_allocator;
Walk_Table(#code {
entry := *table.entries[index];
if entry.hash == hash {
if inline table.compare_function(entry.key, key) {
Basic.array_add(*results, entry.value);
} else {
#if COUNT_COLLISIONS table.find_collisions += 1;
}
} else {
#if COUNT_COLLISIONS table.find_collisions += 1;
}
});
return results;
}
//
// End of deprecated routines.
//
// Remove the first entry at the given key. Returns false if the key was not found.
table_remove :: (table: *Table, key: table.Key_Type) -> (success: bool, value: table.Value_Type) {
if !table.allocated {
dummy: table.Value_Type;
return false, dummy; // Empty table, means not found!
}
Walk_Table(#code {
entry := *table.entries[index];
if (entry.hash == hash) && inline table.compare_function(entry.key, key) {
entry.hash = REMOVED_HASH; // No valid entry will ever hash to REMOVED_HASH.
table.count -= 1;
return true, entry.value;
}
});
dummy: table.Value_Type;
return false, dummy;
}
// table_remove_multiple is a companion function for table_find_multiple.
table_remove_multiple :: (table: *Table, key: table.Key_Type) -> (num_removed: s64) {
if !table.allocated return 0;
orig_count := table.count;
Walk_Table(#code {
entry := *table.entries[index];
if (entry.hash == hash) && inline table.compare_function(entry.key, key) {
entry.hash = REMOVED_HASH; // No valid entry will ever hash to REMOVED_HASH.
table.count -= 1;
}
});
return orig_count - table.count;
}
// find_or_add is kind of like table_set, but used when you
// just want a pointer to the value, which you can fill in.
find_or_add :: (table: *Table, key: table.Key_Type) -> (entry: *table.Value_Type, newly_added: bool) {
value := table_find_pointer(table, key);
if value return value, false;
new_value: table.Value_Type;
value = table_add(table, key, new_value);
return value, true;
}
// We expose these reserved hash values just in case you want
// to write your own hash table iterator.
NEVER_OCCUPIED_HASH :: 0;
REMOVED_HASH :: 1;
FIRST_VALID_HASH :: 2;
// A beta user requested being able to call expand(), so, here:
expand :: (table: *Table, new_allocated := -1) {
old_entries := table.entries;
// If we were adding and removing lots of stuff from the table,
// we might have lots of slots filled with REMOVED_HASH, so,
// in that case, don't grow!
if new_allocated == -1 {
if (table.count * 2 + 1) * 100 < table.allocated * table.LOAD_FACTOR_PERCENT { // The *2 is to say, if we double the size, are we still small enough to fit into the current memory? The reason we are doing this test is, if we removed a bunch of elements, maybe we are full of REMOVED_HASH markers, and if we just rehash to the same size, we can get rid of those. An alternate version (simpler?) might be to check table.count vs table.slots_filled. Note that this becomes less necessary if REFILL_REMOVED is enabled.
// Just go with the current size, but clean out the removals.
new_allocated = table.allocated;
} else {
// We need to go deeper!
new_allocated = table.allocated * 2;
}
} else {
// new_allocated must be a power of two if you pass it manually.
if (new_allocated) Basic.assert(new_allocated & (new_allocated - 1) == 0);
}
if new_allocated < table.SIZE_MIN new_allocated = table.SIZE_MIN;
_internal_resize_memory(table, new_allocated);
table.count = 0; // count and slots_filled will be incremented by table_add.
table.slots_filled = 0;
for * entry, index: old_entries {
// Note that if we removed some stuff, we will over-allocate the next table.
// Maybe we should count the number of clobbers and subtract that. I dunno.
if entry.hash >= FIRST_VALID_HASH table_add(table, entry.key, entry.value);
}
Basic.free(old_entries.data,, table.allocator);
}
next_power_of_two :: inline (x : int) -> int {
// @Speed: This could certainly be faster.
// There is probably something in Hacker's Delight for this.
Basic.assert(x != 0);
p := 1;
while x > p p += p;
return p;
}
#scope_file
_internal_resize_memory :: (using table: *Table, slots_to_allocate: s64 = 0) {
if !table.allocator.proc {
Basic.remember_allocators(table);
}
//
// This is a helper function used by the Table. It sets the table to new memory
// that is ready to be used for hashing, but the caller must retain the old memory
// so that it can hash the items to the new table. We could factor this into
// a user-level resize() that you could call, that would basically be the bottom
// half of expand()... but this function alone will not do enough there.
//
if slots_to_allocate < SIZE_MIN slots_to_allocate = SIZE_MIN;
n := next_power_of_two(slots_to_allocate);
table.allocated = n;
// Right now we align to 8 bytes. Should we handle arbitrary alignment of both
// keys and values? Maybe, but that will complicate the table,
// so let's not worry about it now unless we need it.
// We used to create entries[] uninitialized for performance reasons,
// but this led to problems when people would for example try to assign
// hash tables containing strings to global data; the compiler would
// try to copy the uninitialized strings and crash (because it has no way
// to know they are uninitialized). For now we will go back to zero-initializing
// by putting 'true' here, but I don't love this. -jblow, 24 November 2024.
entries = Basic.NewArray(n, Entry, true,, table.allocator);
for * entries it.hash = 0;
}
Hash :: #import "Hash";
Basic :: #import "Basic";

843
modules/Runtime_Support.jai Normal file
View File

@ -0,0 +1,843 @@
//
// Runtime_Support is less volatile than Preload and contains imports and various biggish code;
// it is considered reasonable that you can mess around with Runtime_Support and ship your own
// version of it.
//
#module_parameters(DEFINE_SYSTEM_ENTRY_POINT: bool, DEFINE_INITIALIZATION: bool, ENABLE_BACKTRACE_ON_CRASH: bool);
#scope_export
OS_IS_UNIX :: OS == .MACOS || OS == .LINUX || OS == .PS5 || OS == .IOS || OS == .ANDROID;
// @Cleanup: __element_duplicate should lose its c_call status, and also be faster...!
__element_duplicate :: (start: *u8, num_elements: s64, size: s64) #c_call #no_aoc {
cursor := start + size;
for 1..num_elements-1 {
memcpy(cursor, start, size);
cursor += size;
}
}
write_nonnegative_number :: (n: u64, base := 10, to_standard_error := false) #no_context #no_abc #no_aoc {
if n == 0 {
write_string("0", to_standard_error);
return;
}
if base > 16 base = 16;
if base < 2 base = 2;
ubase := cast,no_check(u64) base;
// This is of course a horrible hack.
// A u64 in base 2 can be up to 64 digits,
// so we need at least that much space. Add a few more digits for superstition.
buf: [69] u8;
starting_cursor : s32 = buf.count;
cursor := starting_cursor;
while n {
cursor -= 1;
index := n % ubase;
if index <= 9 {
buf[cursor] = cast(u8)(index + #char "0");
} else {
buf[cursor] = cast(u8)(index - 10 + #char "a");
}
n /= ubase;
}
s: string = ---;
s.data = buf.data + cursor;
s.count = starting_cursor - cursor;
write_string(s, to_standard_error);
}
write_number :: (n: s64, base := 10, to_standard_error := false) #no_context #no_aoc {
if n < 0 {
write_string("-", to_standard_error);
n = -n; // Will overflow if this is the most negative s64.
}
write_nonnegative_number(cast,no_check(u64) n, base, to_standard_error = to_standard_error);
}
runtime_support_assertion_failed :: (loc: Source_Code_Location, message: string) -> bool {
write_loc(loc, to_standard_error = true);
write_string(": Assertion failed", to_standard_error = true);
if message {
write_strings(": ", message, "\n", to_standard_error = true);
} else {
write_string("!\n", to_standard_error = true);
}
print_stack_trace :: (node: *Stack_Trace_Node) { // @Copypasta from modules/Basic, but without calling print.
while node {
// There are two different line numbers available.
// node.info.location has a line number, but this is the location of
// the start of the procedure. That may be useful in some cases, but often
// what we really want is the line number of the procedure call, which is
// stored in node.line_number. That's what we print in this case.
if node.info {
write_string(node.info.location.fully_pathed_filename, to_standard_error = true);
write_string(":", to_standard_error = true);
write_nonnegative_number(node.line_number, to_standard_error = true);
write_string(": ", to_standard_error = true);
if node.info.name write_string(node.info.name, to_standard_error = true);
else write_string("(anonymous procedure)", to_standard_error = true);
write_string("\n", to_standard_error = true);
}
node = node.next;
}
}
if context.stack_trace {
write_string("\nStack trace:\n", to_standard_error = true);
print_stack_trace(context.stack_trace);
} else {
write_string("Exiting.\n", to_standard_error = true);
}
report_failed_assertion_more_visibly(loc, message);
__runtime_support_disable_stack_trace = true;
return true;
}
runtime_support_default_logger :: (message: string, data: *void, info: Log_Info) {
if !message then return;
to_standard_error := (info.common_flags & .ERROR) != 0;
if message[message.count-1] != #char "\n" {
write_strings(message, "\n", to_standard_error = to_standard_error);
} else {
write_string(message, to_standard_error = to_standard_error);
}
}
c_style_strlen :: (str: *u8) -> s64 #no_context {
p := str;
while p.* { p += 1; }
return p - str;
}
to_string :: (c_string: *u8) -> string #no_context {
if c_string == null return "";
s: string = ---;
s.data = c_string;
s.count = c_style_strlen(c_string);
return s;
}
// @Incomplete: Pass loc to Runtime_Support versions.
__array_bounds_check_fail :: (index: s64, limit: s64, line_number: s64, filename: *u8) #no_context {
// @Volatile: It is a good idea for these to match the error reports in constant-expression evaluation inside the compiler.
write_string("Array bounds check failed. (The attempted index is ", to_standard_error = true);
write_number(index, to_standard_error = true);
if limit > 0 {
write_string(", but the highest valid index is ", to_standard_error = true);
write_number(limit-1, to_standard_error = true);
} else if limit == 0 {
write_string(", but the array has dimension 0, so no index is valid", to_standard_error = true);
} else {
write_string(", but the array has invalid negative dimension ", to_standard_error = true);
write_number(limit, to_standard_error = true);
}
write_string("). Site is ", to_standard_error = true);
write_string(to_string(filename), to_standard_error = true);
write_string(":", to_standard_error = true);
write_number(line_number, to_standard_error = true);
write_string(".\n", to_standard_error = true);
report_bounds_check_error_more_visibly(index, limit, line_number, filename);
my_panic();
}
__cast_bounds_check_fail :: (pre_value: s64, pre_flags: u32, post_value: s64, post_flags: u32, fatal: u8, line_number: s64, filename: *u8) #no_context {
bits : s32 = 0;
NUMBER_FLAG_SIGNED :: 0x40;
NUMBER_FLAG_8BIT :: 0x100;
NUMBER_FLAG_16BIT :: 0x200;
NUMBER_FLAG_32BIT :: 0x400;
NUMBER_FLAG_64BIT :: 0x800;
if post_flags & NUMBER_FLAG_64BIT bits = 64;
if post_flags & NUMBER_FLAG_32BIT bits = 32;
if post_flags & NUMBER_FLAG_16BIT bits = 16;
if post_flags & NUMBER_FLAG_8BIT bits = 8;
write_string("Cast bounds check failed. Number must be in [", to_standard_error = true);
if post_flags & NUMBER_FLAG_SIGNED {
high_value := (1 << (bits - 1)) - 1;
low_value := ~high_value;
write_number(low_value, to_standard_error = true);
write_string(", ", to_standard_error = true);
write_number(high_value, to_standard_error = true);
} else {
// We don't yet have our stringent definition of left-shift, so, we are generating results
// that will vary on different machines unless we check. So let's check.
high_value: u64;
if bits == 64 {
high_value = 0xffff_ffff_ffff_ffff;
} else {
high_value = ((cast(u64)1) << bits) - 1;
}
write_string("0, ", to_standard_error = true);
write_nonnegative_number(high_value, to_standard_error = true);
}
write_string("]; it was ", to_standard_error = true);
if pre_flags & NUMBER_FLAG_SIGNED {
write_number(pre_value, to_standard_error = true);
} else {
write_nonnegative_number(cast,no_check(u64) pre_value, to_standard_error = true);
}
write_string(". Site is ", to_standard_error = true);
write_string(to_string(filename), to_standard_error = true);
write_string(":", to_standard_error = true);
write_number(line_number, to_standard_error = true);
write_string(".\n", to_standard_error = true);
if fatal my_panic();
}
__null_pointer_check_fail :: (index: s64, line_number: s64, filename: *u8) #no_context {
// @Volatile: It is a good idea for these to match the error reports in constant-expression evaluation inside the compiler.
if index {
// This is a procedure argument.
write_string("Null pointer check failed: Argument ", to_standard_error = true);
write_number(index, to_standard_error = true);
write_string(" is undergoing an automatic dereference, but the pointer is null. Site is ", to_standard_error = true);
} else {
// It was a dereference that happened in some other way.
write_string("Null pointer check failed: A pointer is undergoing an automatic dereference, but the pointer is null. Site is ", to_standard_error = true);
}
write_string(to_string(filename), to_standard_error = true);
write_string(":", to_standard_error = true);
write_number(line_number, to_standard_error = true);
write_string(".\n", to_standard_error = true);
report_null_pointer_error_more_visibly(index, line_number, filename);
my_panic();
}
__arithmetic_overflow :: (left: s64, right: s64, type_code: u16, line_number: s64, filename: *u8) #no_context #no_aoc {
// We have some free bits in type_code...!
fatal := (type_code & 0x8000);
signed := (type_code & 0x4000);
operator_index := (type_code >> 7) & 0x3;
size := (cast(u64)(type_code & 0x000f))*8;
signed_string := ifx signed then "s" else "u";
operator_string := " / ";
if operator_index == 1 then operator_string = " + ";
else if operator_index == 2 then operator_string = " - ";
else if operator_index == 3 then operator_string = " * ";
write_string("Arithmetic overflow. We tried to compute:\n ", to_standard_error = true);
if signed write_number(left, to_standard_error = true);
else write_nonnegative_number(cast,no_check(u64)left, to_standard_error = true);
write_string(operator_string, to_standard_error = true);
if signed write_number(right, to_standard_error = true);
else write_nonnegative_number(cast,no_check(u64)right, to_standard_error = true);
write_strings("\nThe operand type is ", signed_string, to_standard_error = true);
write_nonnegative_number(size, to_standard_error = true);
write_string(", but the result does not fit into this type.\n", to_standard_error = true);
if fatal {
report_arithmetic_overflow_more_visibly(line_number, filename);
my_panic();
}
}
write_string_unsynchronized :: (s: string, to_standard_error := false) #no_context {
#if OS == .WINDOWS {
// @Robustness @Incomplete: Fails if s.count doesn't fit in an u32.
written: u32;
handle := ifx to_standard_error then windows_standard_error else windows_standard_output;
status := WriteFile(handle, s.data, cast(u32) s.count, *written, null);
} else #if OS == .ANDROID {
// write() compiles and runs fine on Android, but stdout and stderr are piped to /dev/null.
// So we need to use __android_log_* if we want these strings to appear anywhere.
// Since __android_log_* functions are line-based, we have to buffer strings until a newline occurs.
// This has the drawback that the writes without a newline are not visible unless they are followed by a newline, eventually.
// But timer-based flushing is too complicated for Runtime_Support. :AndroidWriteString
// -rluba, 2024-01-25
if to_standard_error != android_write_string_buffer_is_error {
// Flush remaining data before switching between stdout and stderr
android_flush_write_string_buffer();
}
android_write_string_buffer_is_error = to_standard_error;
for i: 0..s.count-1 {
c := s[i];
if c == #char "\n" {
android_flush_write_string_buffer(force = true);
} else {
android_write_string_buffer[android_write_string_buffer_offset] = c;
android_write_string_buffer_offset += 1;
if android_write_string_buffer_offset == android_write_string_buffer.count - 1 {
// Buffer is full. We have to flush, even if there’s no newline
android_flush_write_string_buffer();
}
}
}
} else #if OS_IS_UNIX {
STDOUT_FILENO : s32 : 1;
STDERR_FILENO : s32 : 2;
handle := ifx to_standard_error then STDERR_FILENO else STDOUT_FILENO;
written: s64;
while written != s.count {
result := write(handle, s.data + written, cast(u64) (s.count - written));
// We should probably be checking errno for failures,
// but, on macOS at least, we seem to be getting interrupted
// only be EAGAIN.
if result < 0 continue;
written += result;
}
} else #if OS == .WASM {
wasm_write_string :: (count: s64, data: *void, to_standard_error: bool) #foreign; // You will need to provide this function as JS code in your WASM environment. :JaiWasm:
wasm_write_string(s.count, s.data, to_standard_error);
} else {
// Let the user fill this in, if desired.
}
}
write_string :: (s: string, to_standard_error := false) #no_context #compiler {
// write_string is marked #compiler because, if called at compile-time, it
// involves a different implementation that also syncs with the compiler's output.
// This runtime version of write_string is just about syncing the threads
// in your own program.
one_time_init(*synch_initted, init_synchronization());
runtime_support_acquire(*write_string_lock);
write_string_unsynchronized(s, to_standard_error);
runtime_support_release(*write_string_lock);
}
write_strings :: (strings: ..string, to_standard_error := false) #no_context #compiler {
// write_strings is marked #compiler because, if called at compile-time, it
// involves a different implementation that also syncs with the compiler's output.
// This runtime version of write_strings is just about syncing the threads
// in your own program.
one_time_init(*synch_initted, init_synchronization());
runtime_support_acquire(*write_string_lock);
for strings write_string_unsynchronized(it, to_standard_error);
runtime_support_release(*write_string_lock);
}
#scope_module;
write_loc :: (loc: Source_Code_Location, to_standard_error := false) #no_context {
write_strings(loc.fully_pathed_filename, ":", to_standard_error = to_standard_error);
write_number(loc.line_number, to_standard_error = to_standard_error);
write_string(",", to_standard_error);
write_number(loc.character_number, to_standard_error = to_standard_error);
}
my_panic :: () #no_context {
write_string("Panic.\n", to_standard_error = true);
debug_break();
}
__panic_due_to_runtime_call_of_compile_time_procedure :: (line_number: s64, filename: *u8) #no_context {
write_string("Error: Attempt to call a compile-time procedure at runtime. The location of the call was: ", to_standard_error = true);
write_string(to_string(filename), to_standard_error = true);
write_string(":", to_standard_error = true);
write_number(line_number, to_standard_error = true);
write_string(".\n", to_standard_error = true);
my_panic();
}
#if DEFINE_INITIALIZATION {
// TEMPORARY_STORAGE_SIZE is defined by the compiler based on Build_Options.
first_thread_context: #Context;
first_thread_temporary_storage: Temporary_Storage;
first_thread_temporary_storage_data: [TEMPORARY_STORAGE_SIZE] u8 #align 64;
#scope_export
#program_export
__jai_runtime_init :: (argc: s32, argv: **u8) -> *#Context #c_call {
__command_line_arguments.count = argc;
__command_line_arguments.data = argv;
// We don't need this initializer_of call, but we're leaving it in a comment, in case there are
// compiler bugs in Context init and you want to work around them by re-enabling it.
// @Temporary: Putting it back for a bit since there is a bug exhibiting on Mac that might
// also cause first_thread_context not to be fully filled out...
initializer_of(#Context)(*first_thread_context);
ts := *first_thread_temporary_storage;
ts.data = first_thread_temporary_storage_data.data;
ts.size = first_thread_temporary_storage_data.count;
first_thread_context.temporary_storage = ts;
return *first_thread_context;
}
#program_export
__jai_runtime_fini :: (_context: *void) #c_call {
// Nothing here for now!
// We don't bother shutting down the default allocator. The OS frees the memory.
}
}
__instrumentation_first :: () {
// These are places to insert code, from a metaprogram, if you want
// that code to run before main().
}
__instrumentation_second :: () {
}
#scope_module
#if DEFINE_SYSTEM_ENTRY_POINT {
#program_export "main"
__system_entry_point :: (argc: s32, argv: **u8) -> s32 #c_call {
__jai_runtime_init(argc, argv);
push_context first_thread_context {
#if ENABLE_BACKTRACE_ON_CRASH {
// It's possible we should move the init of the crash handler to after __instrumentation_*, so that
// plugins can set it? But, plugin inits can crash. So maybe we need some other way for a plugin
// to say it's taking over the crash handler.
Handler :: #import "Runtime_Support_Crash_Handler";
Handler.init();
}
__instrumentation_first ();
__instrumentation_second();
__program_main :: () #entry_point;
no_inline __program_main();
}
return 0;
}
}
Default_Allocator :: #import "Default_Allocator";
runtime_support_default_allocator_proc :: Default_Allocator.allocator_proc;
#scope_export
compile_time_debug_break :: () #compiler #no_context;
debug_break :: () #no_context {
if #compile_time {
compile_time_debug_break();
} else {
// Break into the debugger, or stop the running process.
#if OS == .PS5 {
#asm { int 0x41; }
} else #if CPU == .X64 {
#asm { int3; }
} else #if CPU == .ARM64 {
#bytes .[0x20, 0x00, 0b001_0_0000, 0b1101_0100]; // BRK 0x01: 11 bit instruction, 16 bit immediate, 5 bit unused.
} else #if OS == .WASM {
wasm_debug_break :: () #foreign; // You will need to provide this function as JS code in your WASM environment. :JaiWasm:
wasm_debug_break();
} else {
llvm_trap :: () #intrinsic "llvm.debugtrap"; // @ToDo: This used to be llvm.trap which supposedly doesn’t work on Android. We need to test if "debugtrap" works there or if we need a different implemenation.
llvm_trap();
}
}
}
// one_time_init is exposed because maybe user libraries want to use a similar thing.
one_time_init :: (synch_value: *s32, to_insert: Code) #expand {
// A routine to run the code in 'to_insert' only once, even if
// there are multiple threads. synch_value.* should be 0 at startup.
// The values of synch_value.*:
// 0 = uninitialized, 1 = in progress init, 2 = initialized.
// Courtesy of Jeff Roberts.
while 1 {
// @Warning: Apparently this read may not work on ARM, we need to look into it.
// The goal here is just to avoid the overhead of spamming the compare_and_swap.
if synch_value.* == 2 break;
success, old := compare_and_swap(synch_value, 0, 1);
if old == {
case 0;
#insert to_insert;
if !compare_and_swap(synch_value, 1, 2) debug_break(); // Should not happen!
case 1;
// Maybe some exponential fall offy thing here?
#if CPU == .X64 {
for 1..4 #asm { pause; pause; pause; pause; pause; }
}
#if CPU == .ARM64 {
#bytes .[0x3F, 0x20, 0x03, 0xD5]; // YIELD
}
case 2;
}
}
}
// @Volatile: Context_Base must match internal compiler settings in general.h
// It must exist and be relatively easy to compile (without requiring #run of
// procedures, for example). Context_Base is going to be annoying to edit because
// if you make an error of some kind, even a simple one like an undeclared identifier,
// you won't get a helpful error message.
Context_Base :: struct {
context_info: *Type_Info_Struct; // Allow libs or DLLs to see what context we are passing them. Will always be initialized to type_of(Context).
thread_index : u32;
allocator := default_allocator;
logger := runtime_support_default_logger;
logger_data : *void;
log_source_identifier: u64; // Arbitrary identifier; settable by the main program.
log_level : Log_Level; // Settable by the main program to inform anyone who logs.
temporary_storage: *Temporary_Storage;
// Currently, for simplicity we don't #if this out right now when _STACK_TRACE is false
// (but may later). It is probably convenient to let runtime code be able
// to check context.stack_trace to see if it is null in some cases,
// rather than needing all that to be #iffed as well. We will see.
stack_trace: *Stack_Trace_Node;
assertion_failed := runtime_support_assertion_failed;
handling_assertion_failure := false; // Used to avoid assert() infinite loops. Do not mess with this value.
default_allocator :: Allocator.{runtime_support_default_allocator_proc, null};
}
Temporary_Storage :: struct { // @Volatile: Must match general.h
data: *u8;
size: s64;
total_bytes_occupied: s64;
high_water_mark: s64;
last_set_mark_location: Source_Code_Location;
implementation_data: [256] u8;
}
#scope_module
init_synchronization :: () #no_context {
// For now, we do not bother to destroy write_string_lock, etc
// on shutdown.
#if OS == .WINDOWS {
InitializeCriticalSection :: (section: *CRITICAL_SECTION_stub) #foreign kernel32;
InitializeCriticalSection(*write_string_lock);
STD_OUTPUT_HANDLE :: -11;
STD_ERROR_HANDLE :: -12;
windows_standard_output = GetStdHandle(STD_OUTPUT_HANDLE);
windows_standard_error = GetStdHandle(STD_ERROR_HANDLE);
} else #if OS == .LINUX || OS == .ANDROID {
// Nothing to do
} else #if OS_IS_UNIX { // Other Unixes
PTHREAD_MUTEX_RECURSIVE :: 2;
#if OS == .MACOS || OS == .IOS {
pthread_mutexattr_t_stub :: struct { memory: [2] s64; }
} else {
pthread_mutexattr_t_stub :: *void;
}
pthread_mutexattr_init :: (attr: *pthread_mutexattr_t_stub) -> s32 #foreign pthreads;
pthread_mutexattr_settype :: (attr: *pthread_mutexattr_t_stub, type: s32) -> s32 #foreign pthreads;
pthread_mutex_init :: (mutex: *pthread_mutex_t_stub, attr: *pthread_mutexattr_t_stub) -> s32 #foreign pthreads;
attr: pthread_mutexattr_t_stub;
pthread_mutexattr_init(*attr);
pthread_mutexattr_settype(*attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(*write_string_lock, *attr); // This had better not fail!
} else {
// Let the user fill this in, if desired.
}
}
#if OS == .WINDOWS {
kernel32 :: #library,system "kernel32";
WriteFile :: (handle: *void, buffer: *void, buffer_length: u32, written_result: *u32, overlapped: *void) -> s32 #foreign kernel32;
GetStdHandle :: (handle: s32) -> *void #foreign kernel32;
EnterCriticalSection :: (lpCriticalSection: *CRITICAL_SECTION_stub) #foreign kernel32;
LeaveCriticalSection :: (lpCriticalSection: *CRITICAL_SECTION_stub) #foreign kernel32;
runtime_support_acquire :: EnterCriticalSection;
runtime_support_release :: LeaveCriticalSection;
write_string_lock: CRITICAL_SECTION_stub;
// Our mutex / critical sections are just dummies to hold as much memory as we need, so we don't bloat Runtime_Support.
CRITICAL_SECTION_stub :: struct {
memory: [5] u64;
}
windows_standard_output: *void;
windows_standard_error: *void;
} else #if OS_IS_UNIX {
libc :: #library,system "libc";
write :: (fd: s32, buf: *void, count: u64) -> s64 #foreign libc;
#if OS == .LINUX || OS == .ANDROID {
// Use futex to avoid depending on pthread
write_string_lock: s32; // Zero if unlocked.
FUTEX_WAIT :: 0;
FUTEX_WAKE :: 1;
FUTEX_PRIVATE_FLAG :: 128;
runtime_support_acquire :: (futex_lock: *s32) #no_context {
SPIN_BUDGET :: 10;
loop_count := 0;
// Retry to acquire the lock until we got it.
while true {
if compare_and_swap(futex_lock, 0, 1) {
// The mutex was free and is now acquired.
return;
}
// Spin wait before trying to acquire the mutex again.
if loop_count < SPIN_BUDGET {
loop_count += 1;
for 0..(1 << loop_count) {
#if CPU == .X64 {
#asm { pause; pause; pause; pause; }
}
#if CPU == .ARM64 {
#bytes .[0x3F, 0x20, 0x03, 0xD5]; // YIELD
}
}
} else {
// Spinning takes too long, let's block and try re-acquire after wake up.
futex(futex_lock, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, 1);
loop_count = 0;
}
}
}
runtime_support_release :: (futex_lock: *s32) #no_context {
compare_and_swap(futex_lock, 1, 0); // lock_xchg would be enough, but we don’t have that intrinsic
futex(futex_lock, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, 1);
}
futex :: (futex_lock: *s32, op: u64, val: u64) #expand {
#if CPU == .X64 {
SYS_futex :: 202;
#asm SYSCALL_SYSRET {
mov.q call: gpr === a, SYS_futex;
mov.q uaddr: gpr === di, futex_lock;
mov.q futex_op: gpr === si, op;
mov.q value: gpr === d, val;
mov.q timeout: gpr === 10, 0;
syscall t1:, t2:, call;
}
} else {
SYS_futex :: 98;
syscall :: (__sysno: s64, __args: ..Any) -> s64 #foreign libc;
syscall(SYS_futex, futex_lock, op, val, null, null, 0);
}
}
} else {
#if OS == .MACOS || OS == .IOS {
pthreads :: #library,system "libc";
} else {
pthreads :: #library,system "libpthread";
}
pthread_mutex_lock :: (mutex: *pthread_mutex_t_stub) -> s32 #foreign pthreads;
pthread_mutex_unlock :: (mutex: *pthread_mutex_t_stub) -> s32 #foreign pthreads;
runtime_support_acquire :: pthread_mutex_lock;
runtime_support_release :: pthread_mutex_unlock;
write_string_lock: pthread_mutex_t_stub;
// Our mutex / critical sections are just dummies to hold as much memory as we need, so we don't bloat Runtime_Support.
// @Volatile: These types have to fit the real declarations on each platform!
#if OS == .MACOS || OS == .IOS {
pthread_mutex_t_stub :: struct {
memory: [64] u8;
}
} else {
pthread_mutex_t_stub :: *void;
}
}
} else {
// Let the user fill this in, if desired.
write_string_lock: void;
runtime_support_acquire :: (lock: *$T) #no_context #expand {}
runtime_support_release :: (lock: *$T) #no_context #expand {}
}
#if OS == .ANDROID {
android_write_string_buffer_offset := 0;
android_write_string_buffer_is_error: bool;
android_write_string_buffer: [512] u8; // Android offers no bare stdio functions, only loggers – which append "\n". So we have to buffer partial lines… :AndroidWriteString
#scope_export
android_flush_write_string_buffer :: (force := false) #no_context {
if !force && !android_write_string_buffer_offset return;
liblog :: #library,system "liblog";
ANDROID_LOG_INFO: s32 : 4;
ANDROID_LOG_ERROR: s32 : 6;
__android_log_write :: (prio: s32, tag: *u8, fmt: *u8, args: .. Any) -> s32 #foreign liblog;
level := ifx android_write_string_buffer_is_error then ANDROID_LOG_ERROR else ANDROID_LOG_INFO;
tag := ifx android_write_string_buffer_is_error then "jai-stderr".data else "jai-stdout".data;
android_write_string_buffer[android_write_string_buffer_offset] = 0;
__android_log_write(level, tag, android_write_string_buffer.data);
android_write_string_buffer_offset = 0;
}
}
synch_initted: s32 = 0;
// ==========================================================================================================================================
// Below are the functions we've added.
#scope_file
report_bounds_check_error_more_visibly :: (index: s64, limit: s64, line_number: s64, filename: *u8) #no_context {
report_error_in_yer_face_no_ctx("Array bounds check failed in file % at line number %: \nProvided index: %, maximum index: %", to_string(filename), line_number, index, limit-1);
}
report_null_pointer_error_more_visibly :: (index: s64, line_number: s64, filename: *u8) #no_context {
if index {
// This is a procedure argument.
report_error_in_yer_face_no_ctx("Null pointer check failed in file % at line number %: \nArgument % is undergoing an automatic dereference, but the pointer is null.", to_string(filename), line_number, index);
} else {
// It was a dereference that happened in some other way.
report_error_in_yer_face_no_ctx("Null pointer check failed in file % at line number %: \nA pointer is undergoing an automatic dereference, but the pointer is null.", to_string(filename), line_number);
}
}
report_arithmetic_overflow_more_visibly :: (line_number: s64, filename: *u8) #no_context {
report_error_in_yer_face_no_ctx("Arithmetic overflow in file % at line number %: \nCheck the logs for more information.", to_string(filename), line_number);
}
report_failed_assertion_more_visibly :: (loc: Source_Code_Location, msg: string) {
report_error_in_yer_face("Assertion failed in file % at line %, char %: \n%", loc.fully_pathed_filename, loc.line_number, loc.character_number, msg);
}
#scope_export
// Set by the program: logs the crash report to the session log and waits for it to be written.
crash_log_hook: #type (crash_report: string);
// Set by the program: the crash report is <folder>-crash-report.txt, beside the executable on
// Windows and in the per-user data folder of this name on Linux and macOS.
crash_report_folder_name := "app";
#scope_file
report_error_in_yer_face_no_ctx :: (message: string, args: .. Any) #no_context {
ctx: #Context;
push_context ctx { report_error_in_yer_face(message, ..args, from_no_context = true); }
}
report_error_in_yer_face :: (message: string, args: .. Any, from_no_context := false) {
using _ :: #import "System"; // 'using' ensures the imports stay within this scope. (Raw imports do not respect scopes)
using _ :: #import "String";
using _ :: #import "Basic";
using _ :: #import "File";
formatted_message := tprint(message, ..args);
builder: String_Builder;
append(*builder, formatted_message);
append(*builder, "\n\n");
if from_no_context || !context.stack_trace {
print_to_builder(*builder, "Stack trace not available%.\n", ifx from_no_context " (it was lost due to a #no_context call)");
} else {
append(*builder, "Stack trace:\n");
node := context.stack_trace;
while node {
if node.info print_to_builder(*builder, "%:%: %\n", node.info.location.fully_pathed_filename, node.line_number, node.info.name);
node = node.next;
}
}
crash_report := builder_to_string(*builder);
// Create a crash report file
data_dir := path_strip_filename(get_path_of_running_executable());
#if OS == .LINUX {
XDG :: #import "XDG";
data_dir = sprint("%/%", XDG.get_path(.DATA_HOME), crash_report_folder_name);
}
#if OS == .MACOS {
using _ :: #import "Objective_C";
application_support := to_string(NSURL.path(NSFileManager.URLForDirectory(NSFileManager.defaultManager(), NSApplicationSupportDirectory, NSUserDomainMask, null, YES, null)));
data_dir = sprint("%/%", application_support, crash_report_folder_name);
}
data_dir = trim_right(data_dir, "/");
make_directory_if_it_does_not_exist(data_dir);
crash_report_file_path := tprint("%/%-crash-report.txt", data_dir, crash_report_folder_name);
write_entire_file(crash_report_file_path, crash_report);
// The crash report is overwritten every new crash, so we also print it to the session log for safekeeping.
// The program's hook queues it and waits for the log to reach the disk; #no_context strips the
// logger from context, so without the hook a #no_context crash can't reach the session log.
if crash_log_hook {
crash_log_hook(crash_report);
} else if !from_no_context {
log_error(crash_report, flags = .TO_FILE_ONLY);
}
// Show a Windows message box
#if OS == .WINDOWS {
W :: #import "Windows";
W_Utf8 :: #import "Windows_Utf8";
W.ShowCursor(1);
W.MessageBoxW(null, W_Utf8.utf8_to_wide(formatted_message,, allocator = temp), W_Utf8.utf8_to_wide("Fatal Error",, allocator = temp), W.MB_OK);
}
}
// END of the functions we've added
// ==========================================================================================================================================

View File

@ -0,0 +1,76 @@
// Secure memory overwrite primitives, extracted from Basic so that allocators can depend on
// wiping without dragging in the rest of Basic. Apart from libsodium it imports Basic.
//
// USE_SODIUM routes the wipe through libsodium's sodium_memzero, the portable way to get a
// store the optimizer is not permitted to elide. Without it, x64 wipes with inline assembly the
// optimizer leaves alone, and other CPUs (arm64) with a plain loop it is allowed to remove.
//
// In this template it defaults to false, so the build needs no libsodium. Module parameters
// only reach a module from the main program, and Basic's arenas import this one directly, so
// this default is what they get. If the app keeps secrets in memory, add the jai-sodium module
// to modules/ and set the default back to true.
#module_parameters () (USE_SODIUM := false, LOAD_TESTS := false);
Basic :: #import "Basic"; // assert
#if USE_SODIUM {
Sodium :: #import "jai-sodium";
// Byte counts are signed ints throughout this module; libsodium wants an unsigned length.
secure_memzero :: inline (memory: *void, count: int) {
if count <= 0 return;
Sodium.sodium_memzero(memory, count.(u64,no_check));
}
}
#if LOAD_TESTS {
#load "tests/secure_memory_tests.jai";
}
// Wipe_Function :: #type (memory: *void, count: int);
// Macros for ease-of-use
secure_overwrite :: (s: string) #expand {
memory_secure_overwrite(s.data, s.count);
}
secure_overwrite :: (a: []$T) #expand {
memory_secure_overwrite(a.data, a.count * size_of(T));
}
secure_overwrite :: (memory: *$T) #expand {
memory_secure_overwrite(memory, size_of(T));
}
secure_overwrite :: (memory: *$T, count: int) #expand {
memory_secure_overwrite(memory, count);
}
// sodium_memzero only zeroes, so a non-zero fill still takes the rep stos / loop path.
memory_secure_overwrite :: inline (memory: *void, count: int, value: u8 = 0) {
Basic.assert(count >= 0);
if count <= 0 return;
#if USE_SODIUM {
if value == 0 {
secure_memzero(memory, count);
return;
}
}
#if CPU == .X64 {
#asm {
memory === di;
value === a;
count === c;
rep_stos.b memory, value, count;
}
} else {
memu8 := cast(*u8) memory;
for 0..count-1 {
memu8[it] = value;
}
}
}

View File

@ -0,0 +1,219 @@
// Test suite for Secure_Memory.
//
// module.jai #loads this file when the module is imported with LOAD_TESTS = true, so the
// suite is built against the same USE_SODIUM setting the module was built with -- running it
// tells you the wipe path you actually shipped works, not the default one.
//
// Every check writes a recognizable pattern, wipes part of it, and asserts both that the
// wiped span changed and that the bytes on either side did not.
#import "Basic"; // String_Builder, print, print_to_builder, tprint
#scope_file
MAX_GROUPS :: 8;
group_names : [MAX_GROUPS] string;
group_checks : [MAX_GROUPS] int;
group_fails : [MAX_GROUPS] int;
group_count := 0;
checks_run := 0;
checks_failed := 0;
// Set by run_secure_memory_tests. One suite is one work item, so a file-scope pointer is
// enough; null means write straight to stdout, as a synchronous or standalone run does.
output_builder: *String_Builder;
emit :: (format_string: string, args: ..Any) {
if output_builder print_to_builder(output_builder, format_string, ..args);
else print(format_string, ..args);
} @PrintLike
group :: (name: string) {
assert(group_count < MAX_GROUPS, "raise MAX_GROUPS");
group_names[group_count] = name;
group_count += 1;
emit("\n-- % --\n", name);
}
expect :: (condition: bool, format: string, args: ..Any, loc := #caller_location) {
checks_run += 1;
if group_count group_checks[group_count-1] += 1;
if condition return;
checks_failed += 1;
if group_count group_fails[group_count-1] += 1;
name := ifx group_count then group_names[group_count-1] else "?";
emit(" FAIL [%:%] %\n", name, loc.line_number, tprint(format, ..args));
}
fill :: (memory: *void, count: s64, value: u8) {
bytes := cast(*u8) memory;
for 0..count-1 bytes[it] = value;
}
all_bytes_are :: (memory: *void, count: s64, value: u8) -> bool {
bytes := cast(*u8) memory;
for 0..count-1 if bytes[it] != value return false;
return true;
}
// A buffer with a canary run on each side, so a wipe that walks past its span is visible.
GUARD :: 16;
BODY :: 64;
CANARY :: 0xa5;
Guarded :: struct {
bytes: [GUARD + BODY + GUARD] u8;
}
body_of :: (using g: *Guarded) -> *u8 {
return bytes.data + GUARD;
}
prepare :: (g: *Guarded, pattern: u8) {
fill(g.bytes.data, g.bytes.count, CANARY);
fill(body_of(g), BODY, pattern);
}
guards_intact :: (g: *Guarded) -> bool {
if !all_bytes_are(g.bytes.data, GUARD, CANARY) return false;
if !all_bytes_are(g.bytes.data + GUARD + BODY, GUARD, CANARY) return false;
return true;
}
// ---------------------------------------------------------------------------
memory_secure_overwrite_tests :: () {
group("memory_secure_overwrite");
g: Guarded;
prepare(*g, 0xcc);
memory_secure_overwrite(body_of(*g), BODY);
expect(all_bytes_are(body_of(*g), BODY, 0), "default value did not zero the body");
expect(guards_intact(*g), "default value ran past the body");
prepare(*g, 0xcc);
memory_secure_overwrite(body_of(*g), BODY, 0xff);
expect(all_bytes_are(body_of(*g), BODY, 0xff), "explicit fill value was not written");
expect(guards_intact(*g), "explicit fill value ran past the body");
// A partial wipe must leave the tail of the body alone, not just the guards.
HALF :: BODY / 2;
prepare(*g, 0xcc);
memory_secure_overwrite(body_of(*g), HALF);
expect(all_bytes_are(body_of(*g), HALF, 0), "partial wipe did not zero the first half");
expect(all_bytes_are(body_of(*g) + HALF, BODY - HALF, 0xcc), "partial wipe clobbered the second half");
expect(guards_intact(*g), "partial wipe ran past the body");
prepare(*g, 0xcc);
memory_secure_overwrite(body_of(*g), 0);
expect(all_bytes_are(body_of(*g), BODY, 0xcc), "a zero count wrote something");
expect(guards_intact(*g), "a zero count ran past the body");
}
secure_overwrite_tests :: () {
group("secure_overwrite overloads");
// string: count is already a byte count.
{
storage: [BODY] u8;
fill(storage.data, storage.count, 0xcc);
s := string.{storage.count, storage.data};
secure_overwrite(s);
expect(all_bytes_are(storage.data, storage.count, 0), "string overload did not zero the backing bytes");
}
// []$T: the overload has to scale count by size_of(T), or it wipes a quarter of the array.
{
values: [16] u32;
for * values it.* = 0xdead_beef;
view: []u32 = values;
secure_overwrite(view);
expect(all_bytes_are(values.data, values.count * size_of(u32), 0), "slice overload did not zero every element");
}
// *$T: wipes exactly size_of(T).
{
Blob :: struct {
a: u64;
b: u64;
c: u64;
}
pair: [2] Blob;
fill(pair.data, size_of(Blob) * 2, 0xcc);
secure_overwrite(*pair[0]);
expect(all_bytes_are(*pair[0], size_of(Blob), 0), "pointer overload did not zero the struct");
expect(all_bytes_are(*pair[1], size_of(Blob), 0xcc), "pointer overload ran into the next struct");
}
// *$T + explicit byte count.
{
g: Guarded;
prepare(*g, 0xcc);
secure_overwrite(body_of(*g), BODY);
expect(all_bytes_are(body_of(*g), BODY, 0), "counted overload did not zero the body");
expect(guards_intact(*g), "counted overload ran past the body");
}
}
#if USE_SODIUM {
sodium_tests :: () {
group("sodium_memzero wrapper");
g: Guarded;
prepare(*g, 0xcc);
secure_memzero(body_of(*g), BODY);
expect(all_bytes_are(body_of(*g), BODY, 0), "secure_memzero did not zero the body");
expect(guards_intact(*g), "secure_memzero ran past the body");
prepare(*g, 0xcc);
secure_memzero(body_of(*g), 0);
expect(all_bytes_are(body_of(*g), BODY, 0xcc), "secure_memzero with a zero count wrote something");
prepare(*g, 0xcc);
secure_memzero(body_of(*g), -1);
expect(all_bytes_are(body_of(*g), BODY, 0xcc), "secure_memzero with a negative count wrote something");
}
}
report :: () -> bool {
emit("\n-- results by group --\n");
for 0..group_count-1 {
if group_fails[it] {
emit(" % -- % checks, % FAILED\n", group_names[it], group_checks[it], group_fails[it]);
} else {
emit(" % -- % checks\n", group_names[it], group_checks[it]);
}
}
emit("\n");
if checks_failed == 0 {
emit("ALL SECURE MEMORY TESTS PASSED (% checks over % groups)\n", checks_run, group_count);
return true;
}
emit("SECURE MEMORY TESTS FAILED (% of % checks over % groups)\n", checks_failed, checks_run, group_count);
return false;
}
#scope_export
// builder null writes to stdout as it goes; pass one to buffer the whole report so a
// threaded run does not interleave with other suites.
run_secure_memory_tests :: (builder: *String_Builder = null) -> bool {
output_builder = builder;
emit("=== Secure_Memory test suite ===\n");
emit("USE_SODIUM = %, CPU = %\n", USE_SODIUM, CPU);
memory_secure_overwrite_tests();
secure_overwrite_tests();
#if USE_SODIUM sodium_tests();
return report();
}

View File

@ -0,0 +1,5 @@
// Capture_File (copied from D:\SnagitIndex\SnagitConverter) imports "Serialization" for Serializer
// and Deserializer. In this app they are part of Basic (modules/Basic/serialization.jai), so this
// module only passes Basic on.
#import "Basic";

View File

@ -0,0 +1,11 @@
// The two example programs here, 'read' and 'write', work in concert.
// Compile those. This file is just here to supply settings to those programs.
NUMBER_OF_MESSAGES :: 10000;
HEADER_SIZE :: 2; // 1 byte for message-or-done, 1 byte for the checksum.
Protocol_Tag :: enum u8 { // Is there a message here, or are we done?
DONE :: 0;
MESSAGE :: 1;
}

View File

@ -0,0 +1,79 @@
#import "Shared_Memory_Channel";
#import "Basic";
// Settings that both 'read' and 'write' use:
#load "common.jai";
CHANNEL_DATA_BYTES :: 65536;
// Note: The checksum is here just to make sure there isn't a bug, not because
// we are worried about bits being corrupted.
main :: () {
success, channel := reader_create_channel("Cowabunga!", CHANNEL_DATA_BYTES);
if !success {
log_error("Exiting due to channel creation failure.\n");
return;
}
defer reader_release_channel(*channel);
write_string("Started reading.\n");
messages_read := 0;
while 1 {
message_success, fatal_error, message := reader_poll_for_message(*channel);
// If message_success == false, it could mean there's just no message ready yet.
if message_success {
// We know, due to our user-level protocol, messages must be at least 1 byte.
// If we get a 0-byte message, that is bad! If we get a 1-byte message, and
// it is not Protocol_Tag.DONE, that is bad! I reordered the checks here
// to make it a bit simpler:
bytes := message.user_read_bytes;
if bytes >= HEADER_SIZE {
command := message.user_read_pointer[0];
if command != cast(u8) Protocol_Tag.MESSAGE {
log_error("Got an invalid command on a data message: %\n", command);
return;
}
stored_checksum := message.user_read_pointer[1];
data_bytes := message.user_read_bytes - HEADER_SIZE;
my_checksum: u8;
for 0..data_bytes-1 {
my_checksum ^= message.user_read_pointer[it + HEADER_SIZE];
}
if my_checksum != stored_checksum {
log_error("Checksum mismatch in message #%: Wanted %, got %. Quitting.\n", messages_read+1, stored_checksum, my_checksum);
return;
}
} else if bytes == 1 {
command := message.user_read_pointer[0];
if command == cast(u8) Protocol_Tag.DONE {
log("Received DONE; breaking from the loop.\n");
break;
}
} else {
log_error("Got a message that is too short! Quitting.\n");
return;
}
reader_done_with_message(*message);
messages_read += 1;
}
if fatal_error {
print("FATAL error!\n");
return;
} else if !message_success {
sleep_milliseconds(1);
}
}
log("Reader has successfully received % messages!\n", messages_read);
assert(messages_read == NUMBER_OF_MESSAGES);
}

Some files were not shown because too many files have changed in this diff Show More