[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:
commit
4323831801
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal 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
125
README.md
Normal 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.
|
||||
8
assets/data/default_global_hotkeys.config
Normal file
8
assets/data/default_global_hotkeys.config
Normal 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
|
||||
7
assets/data/default_global_hotkeys_macos.config
Normal file
7
assets/data/default_global_hotkeys_macos.config
Normal 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
|
||||
25
assets/data/default_hotkeys.config
Normal file
25
assets/data/default_hotkeys.config
Normal 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
|
||||
25
assets/data/default_hotkeys_macos.config
Normal file
25
assets/data/default_hotkeys_macos.config
Normal 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
|
||||
38
assets/data/default_imgui.ini
Normal file
38
assets/data/default_imgui.ini
Normal 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
|
||||
|
||||
83
assets/data/themes/basic-light.theme
Normal file
83
assets/data/themes/basic-light.theme
Normal 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
|
||||
83
assets/data/themes/candy-2.theme
Normal file
83
assets/data/themes/candy-2.theme
Normal 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
|
||||
83
assets/data/themes/candy.theme
Normal file
83
assets/data/themes/candy.theme
Normal 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
|
||||
83
assets/data/themes/catppuccin-frappe.theme
Normal file
83
assets/data/themes/catppuccin-frappe.theme
Normal 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
|
||||
83
assets/data/themes/catppuccin-latte.theme
Normal file
83
assets/data/themes/catppuccin-latte.theme
Normal 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
|
||||
83
assets/data/themes/catppuccin-macchiato.theme
Normal file
83
assets/data/themes/catppuccin-macchiato.theme
Normal 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
|
||||
83
assets/data/themes/catppuccin-mocha.theme
Normal file
83
assets/data/themes/catppuccin-mocha.theme
Normal 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
|
||||
83
assets/data/themes/dsp-lib.theme
Normal file
83
assets/data/themes/dsp-lib.theme
Normal 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
|
||||
83
assets/data/themes/everforest-hard.theme
Normal file
83
assets/data/themes/everforest-hard.theme
Normal 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
|
||||
83
assets/data/themes/gruber-darker.theme
Normal file
83
assets/data/themes/gruber-darker.theme
Normal 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
|
||||
83
assets/data/themes/gruvbox-flat.theme
Normal file
83
assets/data/themes/gruvbox-flat.theme
Normal 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
|
||||
83
assets/data/themes/halogen.theme
Normal file
83
assets/data/themes/halogen.theme
Normal 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
|
||||
83
assets/data/themes/handmade-hero.theme
Normal file
83
assets/data/themes/handmade-hero.theme
Normal 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
|
||||
83
assets/data/themes/jblow-nostalgia.theme
Normal file
83
assets/data/themes/jblow-nostalgia.theme
Normal 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
|
||||
83
assets/data/themes/jblowtorch.theme
Normal file
83
assets/data/themes/jblowtorch.theme
Normal 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
|
||||
83
assets/data/themes/jellybeans.theme
Normal file
83
assets/data/themes/jellybeans.theme
Normal 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
|
||||
83
assets/data/themes/kanagawa.theme
Normal file
83
assets/data/themes/kanagawa.theme
Normal 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
|
||||
83
assets/data/themes/kujukuju.theme
Normal file
83
assets/data/themes/kujukuju.theme
Normal 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
|
||||
83
assets/data/themes/nord-midnight.theme
Normal file
83
assets/data/themes/nord-midnight.theme
Normal 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
|
||||
83
assets/data/themes/nord.theme
Normal file
83
assets/data/themes/nord.theme
Normal 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
|
||||
83
assets/data/themes/purple.theme
Normal file
83
assets/data/themes/purple.theme
Normal 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
|
||||
83
assets/data/themes/redshift-todo.theme
Normal file
83
assets/data/themes/redshift-todo.theme
Normal 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
|
||||
83
assets/data/themes/redshift.theme
Normal file
83
assets/data/themes/redshift.theme
Normal 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
|
||||
83
assets/data/themes/rose-pine-dawn.theme
Normal file
83
assets/data/themes/rose-pine-dawn.theme
Normal 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
|
||||
83
assets/data/themes/rose-pine-moon.theme
Normal file
83
assets/data/themes/rose-pine-moon.theme
Normal 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
|
||||
83
assets/data/themes/rose-pine.theme
Normal file
83
assets/data/themes/rose-pine.theme
Normal 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
|
||||
83
assets/data/themes/solarized-dark.theme
Normal file
83
assets/data/themes/solarized-dark.theme
Normal 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
|
||||
83
assets/data/themes/spacemacs-light.theme
Normal file
83
assets/data/themes/spacemacs-light.theme
Normal 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
|
||||
83
assets/data/themes/steel-breeze.theme
Normal file
83
assets/data/themes/steel-breeze.theme
Normal 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
|
||||
83
assets/data/themes/todo-highlight-1.theme
Normal file
83
assets/data/themes/todo-highlight-1.theme
Normal 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
|
||||
83
assets/data/themes/tokyo-night-light-todo.theme
Normal file
83
assets/data/themes/tokyo-night-light-todo.theme
Normal 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
|
||||
83
assets/data/themes/tokyo-night-light.theme
Normal file
83
assets/data/themes/tokyo-night-light.theme
Normal 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
|
||||
83
assets/data/themes/tokyo-night-todo.theme
Normal file
83
assets/data/themes/tokyo-night-todo.theme
Normal 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
|
||||
83
assets/data/themes/tokyo-night.theme
Normal file
83
assets/data/themes/tokyo-night.theme
Normal 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
|
||||
83
assets/data/themes/vscode-dark.theme
Normal file
83
assets/data/themes/vscode-dark.theme
Normal 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
|
||||
83
assets/data/themes/zenburn.theme
Normal file
83
assets/data/themes/zenburn.theme
Normal 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
|
||||
BIN
assets/fonts/FiraCode-Retina.ttf
Normal file
BIN
assets/fonts/FiraCode-Retina.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/JetBrainsMono-Regular.ttf
Normal file
BIN
assets/fonts/JetBrainsMono-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/OpenSans-Bold.ttf
Normal file
BIN
assets/fonts/OpenSans-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/OpenSans-Regular.ttf
Normal file
BIN
assets/fonts/OpenSans-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/OpenSans-SemiBold.ttf
Normal file
BIN
assets/fonts/OpenSans-SemiBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/OpenSans-SemiBoldItalic.ttf
Normal file
BIN
assets/fonts/OpenSans-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/RobotoMono-Regular.ttf
Normal file
BIN
assets/fonts/RobotoMono-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/images/icon.png
Normal file
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
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
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
BIN
assets/libSDL3.0.dylib
Normal file
Binary file not shown.
50
check_targets.jai
Normal file
50
check_targets.jai
Normal 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
326
first.jai
Normal 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";
|
||||
}
|
||||
1004
modules/Basic/Apollo_Time.jai
Normal file
1004
modules/Basic/Apollo_Time.jai
Normal file
File diff suppressed because it is too large
Load Diff
346
modules/Basic/Array.jai
Normal file
346
modules/Basic/Array.jai
Normal 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
840
modules/Basic/Int128.jai
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1257
modules/Basic/Memory_Debugger.jai
Normal file
1257
modules/Basic/Memory_Debugger.jai
Normal file
File diff suppressed because it is too large
Load Diff
1309
modules/Basic/Print.jai
Normal file
1309
modules/Basic/Print.jai
Normal file
File diff suppressed because it is too large
Load Diff
295
modules/Basic/Simple_String.jai
Normal file
295
modules/Basic/Simple_String.jai
Normal 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;
|
||||
}
|
||||
345
modules/Basic/String_Builder.jai
Normal file
345
modules/Basic/String_Builder.jai
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
252
modules/Basic/Temporary_Storage_Flat_Pool.jai
Normal file
252
modules/Basic/Temporary_Storage_Flat_Pool.jai
Normal 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;
|
||||
}
|
||||
|
||||
257
modules/Basic/Temporary_Storage_Paged_Alloc.jai
Normal file
257
modules/Basic/Temporary_Storage_Paged_Alloc.jai
Normal 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;
|
||||
}
|
||||
|
||||
369
modules/Basic/Visualize_Memory_Debugger.jai
Normal file
369
modules/Basic/Visualize_Memory_Debugger.jai
Normal 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
590
modules/Basic/arena.jai
Normal 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);
|
||||
}
|
||||
}
|
||||
494
modules/Basic/arena_platform.jai
Normal file
494
modules/Basic/arena_platform.jai
Normal 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;
|
||||
}
|
||||
68
modules/Basic/copyrights.txt
Normal file
68
modules/Basic/copyrights.txt
Normal 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
839
modules/Basic/docs/Arena.md
Normal 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.
|
||||
472
modules/Basic/docs/fixed-arena.md
Normal file
472
modules/Basic/docs/fixed-arena.md
Normal 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
64
modules/Basic/enums.jai
Normal 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);
|
||||
}
|
||||
}
|
||||
187
modules/Basic/examples/basic.jai
Normal file
187
modules/Basic/examples/basic.jai
Normal 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");
|
||||
}
|
||||
319
modules/Basic/examples/memory_debugger.jai
Normal file
319
modules/Basic/examples/memory_debugger.jai
Normal 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;
|
||||
}
|
||||
|
||||
189
modules/Basic/examples/time.jai
Normal file
189
modules/Basic/examples/time.jai
Normal 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";
|
||||
244
modules/Basic/fixed_arena.jai
Normal file
244
modules/Basic/fixed_arena.jai
Normal 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;
|
||||
}
|
||||
728
modules/Basic/float_to_string.jai
Normal file
728
modules/Basic/float_to_string.jai
Normal 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
28
modules/Basic/linux.jai
Normal 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
719
modules/Basic/module.jai
Normal 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
60
modules/Basic/osx.jai
Normal 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
59
modules/Basic/posix.jai
Normal 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";
|
||||
144
modules/Basic/serialization.jai
Normal file
144
modules/Basic/serialization.jai
Normal 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);
|
||||
}
|
||||
1068
modules/Basic/string_to_float.jai
Normal file
1068
modules/Basic/string_to_float.jai
Normal file
File diff suppressed because it is too large
Load Diff
713
modules/Basic/tests.jai
Normal file
713
modules/Basic/tests.jai
Normal 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();
|
||||
1725
modules/Basic/tests/arena_tests.jai
Normal file
1725
modules/Basic/tests/arena_tests.jai
Normal file
File diff suppressed because it is too large
Load Diff
925
modules/Basic/tests/fixed_arena_tests.jai
Normal file
925
modules/Basic/tests/fixed_arena_tests.jai
Normal 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
72
modules/Basic/timing.jai
Normal 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;
|
||||
// }
|
||||
// }
|
||||
59
modules/Basic/timing_arm64.jai
Normal file
59
modules/Basic/timing_arm64.jai
Normal 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";
|
||||
29
modules/Basic/timing_macos.jai
Normal file
29
modules/Basic/timing_macos.jai
Normal 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";
|
||||
83
modules/Basic/timing_x64.jai
Normal file
83
modules/Basic/timing_x64.jai
Normal 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
86
modules/Basic/windows.jai
Normal 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";
|
||||
|
||||
167
modules/Capture_File/module.jai
Normal file
167
modules/Capture_File/module.jai
Normal 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";
|
||||
1017
modules/File_Watcher/module.jai
Normal file
1017
modules/File_Watcher/module.jai
Normal file
File diff suppressed because it is too large
Load Diff
576
modules/Hash_Table.jai
Normal file
576
modules/Hash_Table.jai
Normal 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
843
modules/Runtime_Support.jai
Normal 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
|
||||
// ==========================================================================================================================================
|
||||
76
modules/Secure_Memory/module.jai
Normal file
76
modules/Secure_Memory/module.jai
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
219
modules/Secure_Memory/tests/secure_memory_tests.jai
Normal file
219
modules/Secure_Memory/tests/secure_memory_tests.jai
Normal 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();
|
||||
}
|
||||
5
modules/Serialization/module.jai
Normal file
5
modules/Serialization/module.jai
Normal 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";
|
||||
11
modules/Shared_Memory_Channel/examples/common.jai
Normal file
11
modules/Shared_Memory_Channel/examples/common.jai
Normal 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;
|
||||
}
|
||||
|
||||
79
modules/Shared_Memory_Channel/examples/read.jai
Normal file
79
modules/Shared_Memory_Channel/examples/read.jai
Normal 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
Loading…
Reference in New Issue
Block a user