Compare commits

...

74 Commits

Author SHA1 Message Date
e8a79e112c feat: complete Voltex Engine + Survivor game
11 crates, 13 examples (including survivor_game), 255 tests
Phase 1-8 fully implemented + playable survival game demo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:49:24 +09:00
63d5ae2c25 feat(game): add HUD, audio SFX, and game over/restart
Add IMGUI HUD overlay showing HP, wave, score, and enemy count.
Display centered Game Over panel with final score and restart prompt.
Add procedural sine-wave audio clips for shooting (440Hz) and enemy
kills (220Hz) via voltex_audio. Player blinks white during invincibility.
Press R to restart after game over.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:46:41 +09:00
d17732fcd5 feat(game): add collision detection, damage, and scoring
Projectiles destroy enemies on contact (radius check 0.55). Enemies
damage the player on contact with 1s invincibility window. Score +100
per kill. Game over when HP reaches 0. update() now returns FrameEvents
so the caller can trigger audio/visual feedback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:46:34 +09:00
a7cd14f413 feat(game): add enemy spawning with wave system and chase AI
Red sphere enemies spawn at arena edges via a wave system that
escalates each round. Enemies use direct seek steering to chase
the player. Both shadow and color passes render enemies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:41:34 +09:00
5496525a7f feat(game): add projectile shooting toward mouse aim
Left click fires small yellow sphere projectiles from the player toward
the mouse cursor position on the XZ plane. Includes fire cooldown (0.2s),
projectile lifetime (2.0s), and ray-plane intersection for mouse aiming.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:35:46 +09:00
2f60ace70a feat(game): add player movement with WASD and camera follow
Add blue sphere as the player with WASD movement on XZ plane,
clamped to arena bounds. Camera now tracks player position.
Pre-allocate dynamic UBO buffers for 100 entities to support
future enemies and projectiles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:27:11 +09:00
83c97faed0 feat(game): add survivor_game with arena and quarter-view camera
Static arena with 20x20 floor and 4 box obstacles rendered using the
forward PBR pipeline with shadow mapping. Fixed quarter-view camera
at (0, 15, 10) looking at origin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:00:55 +09:00
3897bb0510 fix(editor): improve button colors for visible hover/active feedback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:51:12 +09:00
038398962e fix(editor): replace placeholder font with real 8x12 ASCII bitmap glyphs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:36:19 +09:00
586792b774 docs: add Phase 8-4 editor UI status, spec, and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:26:34 +09:00
2cfb721359 feat(editor): add editor_demo example with IMGUI widgets
Interactive demo showing panel, text, button, slider, and checkbox
widgets rendered via the new UiRenderer pipeline. Uses winit event
loop with mouse input forwarded to UiContext.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:23:31 +09:00
dbaff58a3f feat(editor): add UI renderer pipeline and shader
Add UiRenderer with wgpu render pipeline for 2D UI overlay rendering.
Includes WGSL shader with orthographic projection, alpha blending,
and R8Unorm font atlas sampling. Font pixel (0,0) set to white for
solid-color rect rendering via UV (0,0).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:21:18 +09:00
19db4dd390 feat(editor): add voltex_editor crate with IMGUI core (font, draw_list, widgets)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 15:10:53 +09:00
87b9b7c1bd docs: add Phase 8-3 Lua scripting status, spec, and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:42:21 +09:00
6c3ef334fb feat(script): add voltex_script crate with Lua 5.4 FFI and safe wrapper
Compiles Lua 5.4 from source via cc crate (excluding lua.c/luac.c).
Provides ffi bindings, LuaState safe wrapper, and default engine bindings.
All 9 tests pass (exec, globals, register_fn, voltex_print).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:40:53 +09:00
cde271139f docs: add Phase 8-2 networking status, spec, plan, and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:31:14 +09:00
566990b7af feat(net): add UDP server/client with connection management
Adds NetSocket (non-blocking UdpSocket wrapper with local_addr), NetServer
(connection tracking via HashMap, poll/broadcast/send_to_client), and NetClient
(connect/poll/send/disconnect lifecycle). Includes an integration test on
127.0.0.1:0 that validates ClientConnected, Connected, and UserData receipt
end-to-end with 50ms sleeps to ensure UDP packet delivery.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:29:41 +09:00
4519c5c4a6 feat(net): add voltex_net crate with packet serialization
Introduces the voltex_net crate (no external dependencies) with a binary
Packet protocol over UDP. Supports 6 variants (Connect, Accept, Disconnect,
Ping, Pong, UserData) with a 4-byte header (type_id u8, payload_len u16 LE,
reserved u8) and per-variant payload encoding. Includes 7 unit tests covering
all roundtrips and invalid-type error handling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:29:27 +09:00
e28690b24a docs: add Phase 8-1 AI system status, spec, plan, and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:20:50 +09:00
acaad86aee feat(ai): add steering behaviors (seek, flee, arrive, wander, follow_path)
Implements SteeringAgent and five steering behaviors. truncate() clamps
force magnitude. follow_path() advances waypoints within waypoint_radius
and uses arrive() for the final waypoint. 6 passing tests covering all
behaviors including deceleration and path advancement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:18:50 +09:00
5d0fc9d8d1 feat(ai): add A* pathfinding on NavMesh triangle graph
Implements find_path() using A* over triangle adjacency with XZ heuristic.
Path starts at the given start point, passes through intermediate triangle
centers, and ends at the goal point. Returns None when points are outside
the mesh or no connection exists. 5 passing tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:18:45 +09:00
49957435d7 feat(ai): add voltex_ai crate with NavMesh (manual triangle mesh)
Introduces the voltex_ai crate with NavMesh, NavTriangle structs and
XZ-plane barycentric point-in-triangle helper. Includes find_triangle,
triangle_center, and edge_midpoint utilities with 4 passing tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:18:40 +09:00
cddf9540dd fix(renderer): fix RT shadow shader syntax, experimental features, and R32Float sampler compatibility
- Add 'enable wgpu_ray_query' to RT shadow shader
- Fix RayDesc struct constructor syntax and rayQueryGetCommittedIntersection API
- Enable ExperimentalFeatures in GpuContext for RT
- Change RT shadow texture binding to non-filterable (R32Float)
- Use textureLoad instead of textureSample for RT shadow in lighting pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:06:20 +09:00
b8334ea361 docs: add Phase 7-4 post processing status, spec, plan, and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:55:09 +09:00
6c9fc0cbf1 feat(renderer): add HDR + Bloom + ACES tonemap to deferred_demo
Lighting pass now renders to Rgba16Float HDR target. A 5-level bloom
mip chain (downsample with bright-extract threshold, then additive
upsample) feeds into an ACES tonemap pass that composites the final
LDR output to the swapchain surface. All resources resize correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:53:36 +09:00
4debec43e7 feat(renderer): add bloom/tonemap pipelines and convert lighting to HDR output
- deferred_pipeline.rs: add bloom_bind_group_layout, create_bloom_downsample_pipeline (NO blend),
  create_bloom_upsample_pipeline (additive One+One blend), tonemap_bind_group_layout, create_tonemap_pipeline
- deferred_lighting.wgsl: remove Reinhard tonemap + gamma correction; output raw HDR linear colour
- lib.rs: export new pipeline functions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:49:32 +09:00
6d30151be1 feat(renderer): add bloom and tonemap shaders
- bloom_shader.wgsl: fullscreen vertex + fs_downsample (5-tap box + bright extract) + fs_upsample (9-tap tent)
- tonemap_shader.wgsl: fullscreen vertex + fs_main (HDR+bloom combine, exposure, ACES, gamma 1/2.2)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:48:20 +09:00
6c938999e4 feat(renderer): add HDR target, Bloom resources, and ACES tonemap
- Add hdr.rs with HdrTarget (Rgba16Float render target) and HDR_FORMAT constant
- Add bloom.rs with BloomResources (5-level mip chain), BloomUniform, and mip_sizes()
- Add tonemap.rs with TonemapUniform and CPU-side aces_tonemap() for testing
- Export all new types from lib.rs
- 33 tests passing (26 existing + 3 bloom + 4 tonemap)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:47:19 +09:00
ba610f48dc docs: add Phase 7-1 through 7-3 specs and plans
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:25:11 +09:00
643a329338 docs: add Phase 7-3 RT shadows status and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:25:00 +09:00
a5c0179793 feat(renderer): add hardware RT shadows to deferred_demo
Integrate BLAS/TLAS acceleration structures and RT shadow compute pass
into the deferred rendering demo. Adds GpuContext::new_with_features()
for requesting EXPERIMENTAL_RAY_QUERY, Mesh::new_with_usage() for
BLAS_INPUT buffer flags, and extends the lighting shadow bind group
to 9 entries (shadow map + IBL + SSGI + RT shadow).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:23:34 +09:00
b556cdd768 feat(renderer): add RT shadow compute pipeline and integrate into lighting pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:14:28 +09:00
3a311e14af feat(renderer): add RT shadow resources and compute shader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:14:22 +09:00
e2424bf8c9 feat(renderer): add BLAS/TLAS acceleration structure management for RT
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:14:06 +09:00
71045d8603 docs: add Phase 7-2 SSGI status and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 12:10:21 +09:00
5198e8606f feat(renderer): add SSGI pass to deferred_demo (AO + color bleeding)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 12:08:50 +09:00
3248cd3529 feat(renderer): integrate SSGI output into deferred lighting pass
Adds t_ssgi/s_ssgi bindings (group 2, bindings 5-6) to
lighting_shadow_bind_group_layout and deferred_lighting.wgsl. Replaces the
simple ambient term with SSGI-modulated ambient: ao*ssgi_ao applied to IBL
diffuse+specular, plus ssgi_indirect for indirect color bleeding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 12:05:08 +09:00
eea6194d86 feat(renderer): add SSGI shader and pipeline for screen-space GI
Adds ssgi_shader.wgsl (fullscreen pass: view-space TBN from noise, 64-sample
hemisphere loop with occlusion + color bleeding, outputs vec4(ao, indirect_rgb)).
Adds ssgi_gbuffer_bind_group_layout, ssgi_data_bind_group_layout, and
create_ssgi_pipeline to deferred_pipeline.rs. Exports all three from lib.rs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 12:05:02 +09:00
3d0657885b feat(renderer): add SSGI resources with hemisphere kernel and noise texture
Adds ssgi.rs with SsgiUniform (repr C, Pod), SsgiResources (output texture,
kernel buffer, 4x4 noise texture, uniform buffer), hemisphere kernel generator
(64 samples, z>=0, center-biased), deterministic noise data (16 rotation
vectors), and 3 unit tests. Exports SsgiResources, SsgiUniform,
SSGI_OUTPUT_FORMAT from lib.rs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 12:04:56 +09:00
3839aade62 docs: add Phase 7-1 deferred rendering status and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:55:37 +09:00
49bfa31639 feat(renderer): add deferred_demo example with multi-light deferred rendering
Demonstrates two-pass deferred rendering: G-Buffer pass writes position,
normal, albedo, and material to 4 render targets; lighting pass composites
with 8 animated orbiting point lights plus a directional light using a
fullscreen triangle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:54:01 +09:00
a9b263bf57 feat(renderer): add deferred rendering pipeline (G-Buffer + Lighting pass)
Implements deferred_pipeline.rs with pipeline creation functions:
gbuffer_camera_bind_group_layout (dynamic offset), create_gbuffer_pipeline
(MeshVertex input, 4 color targets), lighting_gbuffer_bind_group_layout
(non-filterable position + filterable rest + NonFiltering sampler),
lighting_lights_bind_group_layout, lighting_shadow_bind_group_layout, and
create_lighting_pipeline (FullscreenVertex, no depth stencil).
All wgpu 28.0 fields (immediate_size, multiview_mask, cache) included.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:49:45 +09:00
f6912be248 feat(renderer): add deferred lighting pass shader with Cook-Torrance BRDF
Implements deferred_lighting.wgsl for the fullscreen lighting pass:
reads G-Buffer textures, runs the identical Cook-Torrance BRDF functions
(distribution_ggx, geometry_smith, fresnel_schlick, attenuation, etc.)
from pbr_shader.wgsl, computes multi-light contribution with PCF shadow
and IBL, then applies Reinhard tonemapping and gamma correction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:49:38 +09:00
72fa401420 feat(renderer): add G-Buffer pass shader for deferred rendering
Implements deferred_gbuffer.wgsl for the geometry pass: samples albedo
and normal map textures, applies TBN normal mapping, and writes world
position, encoded normal, albedo, and material parameters (metallic/
roughness/ao) to 4 separate G-Buffer render targets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:49:32 +09:00
03b1419b17 feat(renderer): add GBuffer and fullscreen triangle for deferred rendering
Introduces GBuffer struct with 4 render target TextureViews (position/
normal/albedo/material) plus depth, and a fullscreen oversized triangle
for screen-space passes. Exports format constants and create helpers.
Updates lib.rs with new module declarations and re-exports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:49:27 +09:00
2b3e3a6a5e docs: add Phase 5-1 through 6-3 specs, plans, and Cargo.lock
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:37:16 +09:00
0991f74275 docs: add Phase 6-3 mixer status and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:36:24 +09:00
26e4d7fa4f feat(audio): integrate mixer groups into mixing pipeline and AudioSystem
Wires MixerState into mix_sounds (group × master volume applied per sound)
and exposes SetGroupVolume/FadeGroup commands through AudioSystem.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:35:05 +09:00
261e52a752 feat(audio): add MixGroup, GroupState, and MixerState with fade support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:32:33 +09:00
56c5aff483 docs: add Phase 6-2 3D audio status and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:28:40 +09:00
7150924d5d feat(audio): add play_3d and set_listener to AudioSystem
Add Play3d and SetListener variants to AudioCommand, expose play_3d
and set_listener methods on AudioSystem, initialize a Listener in
the Windows audio thread, and handle the new commands in the match.
Update mix_sounds call to pass the listener.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:27:34 +09:00
bd044c6653 feat(audio): integrate spatial 3D audio into mixing pipeline
Add SpatialParams field to PlayingSound, new_3d constructor, and
listener parameter to mix_sounds. Compute per-channel attenuation
and stereo panning when spatial params are present; 2D sounds are
unchanged. Add three new tests: spatial_2d_unchanged,
spatial_far_away_silent, and spatial_right_panning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:27:29 +09:00
4436382baf feat(audio): add 3D audio spatial functions (distance attenuation, stereo panning)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:24:55 +09:00
7ba3d5758b docs: add Phase 6-1 audio system status and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:13:18 +09:00
a1a90ae4f8 feat(audio): add audio_demo example with sine wave playback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:11:55 +09:00
6de5681707 feat(audio): add AudioSystem with WASAPI audio thread
Introduces AudioCommand enum (Play, Stop, SetVolume, StopAll, Shutdown)
and AudioSystem that spawns a dedicated audio thread. On Windows the
thread drives WasapiDevice with a 5ms mix-and-write loop; on other
platforms it runs in silent null mode. lib.rs exports wasapi (windows)
and audio_system modules with AudioSystem re-exported at crate root.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:10:28 +09:00
4cda9d54f3 feat(audio): add WASAPI FFI bindings for Windows audio output
Implements COM vtable structs for IMMDeviceEnumerator, IMMDevice,
IAudioClient, and IAudioRenderClient with correct IUnknown base layout.
WasapiDevice handles COM init, default endpoint activation, mix format
detection (float/i16), shared-mode Initialize (50ms buffer), and
write_samples with GetCurrentPadding/GetBuffer/ReleaseBuffer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:10:19 +09:00
f52186f732 feat(audio): add mixing functions with volume, looping, and channel conversion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:59:17 +09:00
f0646c34eb feat(audio): add WAV parser with PCM 16-bit support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:59:13 +09:00
dc12715279 feat(audio): add voltex_audio crate with AudioClip type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:59:10 +09:00
75ec3b308f docs: add Phase 5-3 raycasting status and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:49:19 +09:00
67273834d6 feat(physics): add BVH-accelerated raycast with ECS integration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:47:52 +09:00
3f12c4661c feat(physics): add ray intersection tests (AABB, sphere, box)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:46:29 +09:00
d3eead53cf feat(math): add Ray type with direction normalization 2026-03-25 10:45:06 +09:00
102304760e docs: add Phase 5-2 rigid body simulation status and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:40:13 +09:00
b516984015 feat(physics): add impulse collision response and physics_step
Implements resolve_collisions() with impulse-based velocity correction and
Baumgarte-style position correction, plus physics_step() combining integrate,
detect_collisions, and resolve_collisions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:38:42 +09:00
b8c3b6422c feat(physics): add Semi-implicit Euler integration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:36:17 +09:00
9bdb502f8f feat(physics): add RigidBody component and PhysicsConfig 2026-03-25 10:35:16 +09:00
7d91e204cc docs: add Phase 5-1 collision detection status and deferred items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:27:13 +09:00
c1c84cdbff feat(physics): add detect_collisions ECS integration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:17:26 +09:00
0570d3c4ba feat(physics): add BVH tree for broad phase collision detection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:15:24 +09:00
74694315a6 feat(physics): add narrow phase collision detection (sphere-sphere, sphere-box, box-box)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:14:05 +09:00
cc24b19f93 feat(physics): add voltex_physics crate with Collider and ContactPoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:12:22 +09:00
8d3855c1a7 feat(math): add AABB type with intersection, merge, and containment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:10:27 +09:00
177 changed files with 53582 additions and 11 deletions

View File

@@ -0,0 +1,53 @@
{
"permissions": {
"allow": [
"Bash(export PATH=\"$HOME/.cargo/bin:$PATH\")",
"Bash(cargo test:*)",
"Bash(where link.exe)",
"Bash(rustup show:*)",
"Bash(cargo clean:*)",
"Read(//c/Program Files/Microsoft Visual Studio/**)",
"Read(//c/Program Files \\(x86\\)/Microsoft Visual Studio/**)",
"Read(//c//**)",
"Read(//c/Users/SSAFY/miniforge3/Library/bin/**)",
"Read(//c/Users/SSAFY/miniforge3/Library/mingw-w64/**)",
"Read(//c/Users/SSAFY/miniforge3/**)",
"Bash(ls C:/Users/SSAFY/miniforge3/Library/bin/ld*)",
"Bash(ls C:/Users/SSAFY/miniforge3/Library/mingw-w64/bin/ld*)",
"Bash(conda install:*)",
"Bash(C:/Users/SSAFY/miniforge3/Scripts/conda.exe install:*)",
"Bash(export PATH=$HOME/.cargo/bin:$PATH)",
"Read(//c/Users/SSAFY/Desktop/projects/voltex/**)",
"Bash(export PATH=\"$HOME/.cargo/bin:/c/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64:$PATH\")",
"Bash(cmd //C \"call \\\\\"C:\\\\\\\\Program Files\\\\\\\\Microsoft Visual Studio\\\\\\\\2022\\\\\\\\Community\\\\\\\\VC\\\\\\\\Auxiliary\\\\\\\\Build\\\\\\\\vcvarsall.bat\\\\\" x64 >nul 2>&1 && echo LIB=%LIB% && echo INCLUDE=%INCLUDE%\")",
"Bash(ls '/c/Program Files \\(x86\\)/Windows Kits/')",
"Bash(find '/c/Program Files \\(x86\\)/Windows Kits' -name kernel32.lib)",
"Bash(find '/c/Program Files \\(x86\\)/Windows Kits/10' -name kernel32.lib)",
"Bash(ls:*)",
"Bash(export PATH=\"$HOME/.cargo/bin:$PATH\" MSVC_LIB=\"/c/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.44.35207/lib/x64\" SDK_UM=\"/c/Program Files \\(x86\\)/Windows Kits/10/Lib/10.0.26100.0/um/x64\" SDK_UCRT=\"/c/Program Files \\(x86\\)/Windows Kits/10/Lib/10.0.26100.0/ucrt/x64\" export LIB=\"$MSVC_LIB;$SDK_UM;$SDK_UCRT\" __NEW_LINE_dfaf9ba8c4cc16a9__ MSVC_INC=\"/c/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.44.35207/include\" SDK_INC=\"/c/Program Files \\(x86\\)/Windows Kits/10/Include/10.0.26100.0/ucrt\" SDK_UM_INC=\"/c/Program Files \\(x86\\)/Windows Kits/10/Include/10.0.26100.0/um\" SDK_SHARED_INC=\"/c/Program Files \\(x86\\)/Windows Kits/10/Include/10.0.26100.0/shared\" export INCLUDE=\"$MSVC_INC;$SDK_INC;$SDK_UM_INC;$SDK_SHARED_INC\" __NEW_LINE_dfaf9ba8c4cc16a9__ export PATH=\"/c/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64:$PATH\" __NEW_LINE_dfaf9ba8c4cc16a9__ cargo test --workspace)",
"Bash(find '/c/Program Files \\(x86\\)/Windows Kits' -name dbghelp.lib)",
"Bash(find /c/Users/SSAFY/Desktop/projects/voltex -name *.rs -path */examples*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(cargo build:*)",
"Bash(cargo run:*)",
"Bash(cargo doc:*)",
"Bash(cargo metadata:*)",
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); pkgs=[p for p in d[''''packages''''] if p[''''name'''']==''''wgpu'''']; feats=pkgs[0][''''features''''] if pkgs else {}; print\\([f for f in feats if ''''ray'''' in f.lower\\(\\)]\\)\")",
"Read(//c/Users/SSAFY/Desktop/projects/voltex/$HOME/.cargo/registry/src/*/wgpu-28.*/**)",
"Bash(find /c/Users/SSAFY/.cargo/registry/src -name *.rs -path *wgpu_types*)",
"Bash(xargs grep:*)",
"Bash(find /c/Users/SSAFY/.cargo/registry/src -name *.rs)",
"Bash(find /c/Users/SSAFY/.cargo/registry/src -name *.rs -path *wgpu*)",
"Bash(find /c/Users/SSAFY/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wgpu-28.0.0/src -name *.rs)",
"Bash(grep -r ExperimentalFeatures C:/Users/SSAFY/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wgpu-core-*/src/)",
"Bash(curl -sL https://www.lua.org/ftp/lua-5.4.7.tar.gz -o /tmp/lua-5.4.7.tar.gz)",
"Read(//tmp/**)",
"Bash(mkdir -p C:/Users/SSAFY/Desktop/projects/voltex/crates/voltex_script/lua)",
"Read(//c/tmp/**)",
"Bash(tar xzf:*)",
"Bash(cp lua-5.4.7/src/*.c lua-5.4.7/src/*.h C:/Users/SSAFY/Desktop/projects/voltex/crates/voltex_script/lua/)",
"Bash(cd:*)"
]
}
}

95
Cargo.lock generated
View File

@@ -180,6 +180,13 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "audio_demo"
version = "0.1.0"
dependencies = [
"voltex_audio",
]
[[package]]
name = "autocfg"
version = "1.5.0"
@@ -425,6 +432,21 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
[[package]]
name = "deferred_demo"
version = "0.1.0"
dependencies = [
"bytemuck",
"env_logger",
"log",
"pollster",
"voltex_math",
"voltex_platform",
"voltex_renderer",
"wgpu",
"winit",
]
[[package]]
name = "dispatch"
version = "0.2.0"
@@ -461,6 +483,21 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]]
name = "editor_demo"
version = "0.1.0"
dependencies = [
"bytemuck",
"env_logger",
"log",
"pollster",
"voltex_editor",
"voltex_platform",
"voltex_renderer",
"wgpu",
"winit",
]
[[package]]
name = "env_filter"
version = "1.0.1"
@@ -1840,6 +1877,23 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]]
name = "survivor_game"
version = "0.1.0"
dependencies = [
"bytemuck",
"env_logger",
"log",
"pollster",
"voltex_audio",
"voltex_editor",
"voltex_math",
"voltex_platform",
"voltex_renderer",
"wgpu",
"winit",
]
[[package]]
name = "syn"
version = "2.0.117"
@@ -2022,10 +2076,24 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "voltex_ai"
version = "0.1.0"
dependencies = [
"voltex_math",
]
[[package]]
name = "voltex_asset"
version = "0.1.0"
[[package]]
name = "voltex_audio"
version = "0.1.0"
dependencies = [
"voltex_math",
]
[[package]]
name = "voltex_ecs"
version = "0.1.0"
@@ -2033,10 +2101,30 @@ dependencies = [
"voltex_math",
]
[[package]]
name = "voltex_editor"
version = "0.1.0"
dependencies = [
"bytemuck",
"wgpu",
]
[[package]]
name = "voltex_math"
version = "0.1.0"
[[package]]
name = "voltex_net"
version = "0.1.0"
[[package]]
name = "voltex_physics"
version = "0.1.0"
dependencies = [
"voltex_ecs",
"voltex_math",
]
[[package]]
name = "voltex_platform"
version = "0.1.0"
@@ -2059,6 +2147,13 @@ dependencies = [
"winit",
]
[[package]]
name = "voltex_script"
version = "0.1.0"
dependencies = [
"cc",
]
[[package]]
name = "walkdir"
version = "2.5.0"

View File

@@ -15,6 +15,16 @@ members = [
"examples/multi_light_demo",
"examples/shadow_demo",
"examples/ibl_demo",
"examples/deferred_demo",
"crates/voltex_physics",
"crates/voltex_audio",
"examples/audio_demo",
"crates/voltex_ai",
"crates/voltex_net",
"crates/voltex_script",
"crates/voltex_editor",
"examples/editor_demo",
"examples/survivor_game",
]
[workspace.dependencies]
@@ -23,6 +33,12 @@ voltex_platform = { path = "crates/voltex_platform" }
voltex_renderer = { path = "crates/voltex_renderer" }
voltex_ecs = { path = "crates/voltex_ecs" }
voltex_asset = { path = "crates/voltex_asset" }
voltex_physics = { path = "crates/voltex_physics" }
voltex_audio = { path = "crates/voltex_audio" }
voltex_ai = { path = "crates/voltex_ai" }
voltex_net = { path = "crates/voltex_net" }
voltex_script = { path = "crates/voltex_script" }
voltex_editor = { path = "crates/voltex_editor" }
wgpu = "28.0"
winit = "0.30"
bytemuck = { version = "1", features = ["derive"] }

View File

@@ -0,0 +1,7 @@
[package]
name = "voltex_ai"
version = "0.1.0"
edition = "2021"
[dependencies]
voltex_math.workspace = true

View File

@@ -0,0 +1,7 @@
pub mod navmesh;
pub mod pathfinding;
pub mod steering;
pub use navmesh::{NavMesh, NavTriangle};
pub use pathfinding::find_path;
pub use steering::{SteeringAgent, seek, flee, arrive, wander, follow_path};

View File

@@ -0,0 +1,158 @@
use voltex_math::Vec3;
/// A triangle in the navigation mesh, defined by vertex indices and neighbor triangle indices.
#[derive(Debug, Clone)]
pub struct NavTriangle {
/// Indices into NavMesh::vertices
pub indices: [usize; 3],
/// Neighboring triangle indices (None if no neighbor on that edge)
pub neighbors: [Option<usize>; 3],
}
/// Navigation mesh composed of triangles for pathfinding.
pub struct NavMesh {
pub vertices: Vec<Vec3>,
pub triangles: Vec<NavTriangle>,
}
impl NavMesh {
pub fn new(vertices: Vec<Vec3>, triangles: Vec<NavTriangle>) -> Self {
Self { vertices, triangles }
}
/// Find the index of the triangle that contains `point` (XZ plane test).
/// Returns None if the point is outside all triangles.
pub fn find_triangle(&self, point: Vec3) -> Option<usize> {
for (i, tri) in self.triangles.iter().enumerate() {
let a = self.vertices[tri.indices[0]];
let b = self.vertices[tri.indices[1]];
let c = self.vertices[tri.indices[2]];
if point_in_triangle_xz(point, a, b, c) {
return Some(i);
}
}
None
}
/// Return the centroid of triangle `tri_idx` in 3D space.
pub fn triangle_center(&self, tri_idx: usize) -> Vec3 {
let tri = &self.triangles[tri_idx];
let a = self.vertices[tri.indices[0]];
let b = self.vertices[tri.indices[1]];
let c = self.vertices[tri.indices[2]];
Vec3::new(
(a.x + b.x + c.x) / 3.0,
(a.y + b.y + c.y) / 3.0,
(a.z + b.z + c.z) / 3.0,
)
}
/// Return the midpoint of the shared edge between `tri_idx` and neighbor slot `edge_idx` (0, 1, or 2).
/// Edge 0 is between vertex indices[0] and indices[1],
/// Edge 1 is between indices[1] and indices[2],
/// Edge 2 is between indices[2] and indices[0].
pub fn edge_midpoint(&self, tri_idx: usize, edge_idx: usize) -> Vec3 {
let tri = &self.triangles[tri_idx];
let (i0, i1) = match edge_idx {
0 => (tri.indices[0], tri.indices[1]),
1 => (tri.indices[1], tri.indices[2]),
2 => (tri.indices[2], tri.indices[0]),
_ => panic!("edge_idx must be 0, 1, or 2"),
};
let a = self.vertices[i0];
let b = self.vertices[i1];
Vec3::new(
(a.x + b.x) / 2.0,
(a.y + b.y) / 2.0,
(a.z + b.z) / 2.0,
)
}
}
/// Test whether `point` lies inside or on the triangle (a, b, c) using XZ barycentric coordinates.
pub fn point_in_triangle_xz(point: Vec3, a: Vec3, b: Vec3, c: Vec3) -> bool {
// Compute barycentric coordinates using XZ plane
let px = point.x;
let pz = point.z;
let ax = a.x; let az = a.z;
let bx = b.x; let bz = b.z;
let cx = c.x; let cz = c.z;
let denom = (bz - cz) * (ax - cx) + (cx - bx) * (az - cz);
if denom.abs() < f32::EPSILON {
return false; // degenerate triangle
}
let u = ((bz - cz) * (px - cx) + (cx - bx) * (pz - cz)) / denom;
let v = ((cz - az) * (px - cx) + (ax - cx) * (pz - cz)) / denom;
let w = 1.0 - u - v;
u >= 0.0 && v >= 0.0 && w >= 0.0
}
#[cfg(test)]
mod tests {
use super::*;
fn make_simple_navmesh() -> NavMesh {
// Two triangles sharing an edge:
// v0=(0,0,0), v1=(2,0,0), v2=(1,0,2), v3=(3,0,2)
// Tri 0: v0, v1, v2 (left triangle)
// Tri 1: v1, v3, v2 (right triangle)
let vertices = vec![
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 2.0),
Vec3::new(3.0, 0.0, 2.0),
];
let triangles = vec![
NavTriangle {
indices: [0, 1, 2],
neighbors: [None, Some(1), None],
},
NavTriangle {
indices: [1, 3, 2],
neighbors: [None, None, Some(0)],
},
];
NavMesh::new(vertices, triangles)
}
#[test]
fn test_find_triangle_inside() {
let nm = make_simple_navmesh();
// Center of tri 0 is roughly (1, 0, 0.67)
let p = Vec3::new(1.0, 0.0, 0.5);
let result = nm.find_triangle(p);
assert_eq!(result, Some(0));
}
#[test]
fn test_find_triangle_outside() {
let nm = make_simple_navmesh();
// Point far outside the mesh
let p = Vec3::new(10.0, 0.0, 10.0);
let result = nm.find_triangle(p);
assert_eq!(result, None);
}
#[test]
fn test_triangle_center() {
let nm = make_simple_navmesh();
let center = nm.triangle_center(0);
// (0+2+1)/3 = 1.0, (0+0+0)/3 = 0.0, (0+0+2)/3 = 0.667
assert!((center.x - 1.0).abs() < 1e-5);
assert!((center.y - 0.0).abs() < 1e-5);
assert!((center.z - (2.0 / 3.0)).abs() < 1e-5);
}
#[test]
fn test_edge_midpoint() {
let nm = make_simple_navmesh();
// Edge 1 of tri 0 is between indices[1]=v1=(2,0,0) and indices[2]=v2=(1,0,2)
let mid = nm.edge_midpoint(0, 1);
assert!((mid.x - 1.5).abs() < 1e-5);
assert!((mid.y - 0.0).abs() < 1e-5);
assert!((mid.z - 1.0).abs() < 1e-5);
}
}

View File

@@ -0,0 +1,241 @@
use std::collections::BinaryHeap;
use std::cmp::Ordering;
use voltex_math::Vec3;
use crate::navmesh::NavMesh;
/// Node for A* priority queue (min-heap by f_cost, then g_cost).
#[derive(Debug, Clone)]
struct AStarNode {
tri_idx: usize,
g_cost: f32,
f_cost: f32,
parent: Option<usize>,
}
impl PartialEq for AStarNode {
fn eq(&self, other: &Self) -> bool {
self.f_cost == other.f_cost && self.g_cost == other.g_cost
}
}
impl Eq for AStarNode {}
// BinaryHeap is a max-heap; we negate costs to get min-heap behavior.
impl PartialOrd for AStarNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for AStarNode {
fn cmp(&self, other: &Self) -> Ordering {
// Reverse ordering: lower f_cost = higher priority
other.f_cost.partial_cmp(&self.f_cost)
.unwrap_or(Ordering::Equal)
.then_with(|| other.g_cost.partial_cmp(&self.g_cost).unwrap_or(Ordering::Equal))
}
}
/// XZ distance between two Vec3 points (ignoring Y).
pub fn distance_xz(a: Vec3, b: Vec3) -> f32 {
let dx = a.x - b.x;
let dz = a.z - b.z;
(dx * dx + dz * dz).sqrt()
}
/// Find a path from `start` to `goal` on the given NavMesh using A*.
///
/// Returns Some(path) where path[0] == start, path[last] == goal, and
/// intermediate points are triangle centers. Returns None if either
/// point is outside the mesh or no path exists.
pub fn find_path(navmesh: &NavMesh, start: Vec3, goal: Vec3) -> Option<Vec<Vec3>> {
let start_tri = navmesh.find_triangle(start)?;
let goal_tri = navmesh.find_triangle(goal)?;
// If start and goal are in the same triangle, path is direct.
if start_tri == goal_tri {
return Some(vec![start, goal]);
}
let n = navmesh.triangles.len();
// g_cost[i] = best known cost to reach triangle i
let mut g_costs = vec![f32::INFINITY; n];
// parent[i] = index of parent triangle in the A* tree
let mut parents: Vec<Option<usize>> = vec![None; n];
let mut visited = vec![false; n];
let goal_center = navmesh.triangle_center(goal_tri);
g_costs[start_tri] = 0.0;
let start_center = navmesh.triangle_center(start_tri);
let h = distance_xz(start_center, goal_center);
let mut open = BinaryHeap::new();
open.push(AStarNode {
tri_idx: start_tri,
g_cost: 0.0,
f_cost: h,
parent: None,
});
while let Some(node) = open.pop() {
let idx = node.tri_idx;
if visited[idx] {
continue;
}
visited[idx] = true;
parents[idx] = node.parent;
if idx == goal_tri {
// Reconstruct path
let mut tri_path = Vec::new();
let mut cur = idx;
loop {
tri_path.push(cur);
match parents[cur] {
Some(p) => cur = p,
None => break,
}
}
tri_path.reverse();
// Convert triangle path to Vec3 waypoints:
// start point -> intermediate triangle centers -> goal point
let mut path = Vec::new();
path.push(start);
// skip first (start_tri) and last (goal_tri) in intermediate centers
for &ti in &tri_path[1..tri_path.len() - 1] {
path.push(navmesh.triangle_center(ti));
}
path.push(goal);
return Some(path);
}
let tri = &navmesh.triangles[idx];
let current_center = navmesh.triangle_center(idx);
for neighbor_opt in &tri.neighbors {
if let Some(nb_idx) = *neighbor_opt {
if visited[nb_idx] {
continue;
}
let nb_center = navmesh.triangle_center(nb_idx);
let tentative_g = g_costs[idx] + distance_xz(current_center, nb_center);
if tentative_g < g_costs[nb_idx] {
g_costs[nb_idx] = tentative_g;
let h_nb = distance_xz(nb_center, goal_center);
open.push(AStarNode {
tri_idx: nb_idx,
g_cost: tentative_g,
f_cost: tentative_g + h_nb,
parent: Some(idx),
});
}
}
}
}
None // No path found
}
#[cfg(test)]
mod tests {
use super::*;
use crate::navmesh::{NavMesh, NavTriangle};
/// Build a 3-triangle strip along Z axis:
/// v0=(0,0,0), v1=(2,0,0), v2=(0,0,2), v3=(2,0,2), v4=(0,0,4), v5=(2,0,4)
/// Tri0: v0,v1,v2 — Tri1: v1,v3,v2 — Tri2: v2,v3,v4... wait, need a strip
///
/// Simpler strip:
/// Tri0: (0,0,0),(2,0,0),(1,0,2) center ~(1,0,0.67)
/// Tri1: (2,0,0),(3,0,2),(1,0,2) center ~(2,0,1.33)
/// Tri2: (1,0,2),(3,0,2),(2,0,4) center ~(2,0,2.67)
fn make_strip() -> NavMesh {
let vertices = vec![
Vec3::new(0.0, 0.0, 0.0), // 0
Vec3::new(2.0, 0.0, 0.0), // 1
Vec3::new(1.0, 0.0, 2.0), // 2
Vec3::new(3.0, 0.0, 2.0), // 3
Vec3::new(2.0, 0.0, 4.0), // 4
];
let triangles = vec![
NavTriangle { indices: [0, 1, 2], neighbors: [None, Some(1), None] },
NavTriangle { indices: [1, 3, 2], neighbors: [None, Some(2), Some(0)] },
NavTriangle { indices: [2, 3, 4], neighbors: [Some(1), None, None] },
];
NavMesh::new(vertices, triangles)
}
fn make_disconnected() -> NavMesh {
// Two separate triangles not connected
let vertices = vec![
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 2.0),
Vec3::new(10.0, 0.0, 10.0),
Vec3::new(12.0, 0.0, 10.0),
Vec3::new(11.0, 0.0, 12.0),
];
let triangles = vec![
NavTriangle { indices: [0, 1, 2], neighbors: [None, None, None] },
NavTriangle { indices: [3, 4, 5], neighbors: [None, None, None] },
];
NavMesh::new(vertices, triangles)
}
#[test]
fn test_path_same_triangle() {
let nm = make_strip();
let start = Vec3::new(0.5, 0.0, 0.5);
let goal = Vec3::new(1.5, 0.0, 0.5);
let path = find_path(&nm, start, goal).expect("should find path");
assert_eq!(path.len(), 2);
assert_eq!(path[0], start);
assert_eq!(path[path.len() - 1], goal);
}
#[test]
fn test_path_adjacent_triangles() {
let nm = make_strip();
// start in tri0, goal in tri1
let start = Vec3::new(0.8, 0.0, 0.5);
let goal = Vec3::new(2.5, 0.0, 1.5);
let path = find_path(&nm, start, goal).expect("should find path");
assert!(path.len() >= 2);
assert_eq!(path[0], start);
assert_eq!(path[path.len() - 1], goal);
}
#[test]
fn test_path_three_triangle_strip() {
let nm = make_strip();
// start in tri0, goal in tri2
let start = Vec3::new(0.8, 0.0, 0.5);
let goal = Vec3::new(2.0, 0.0, 3.5);
let path = find_path(&nm, start, goal).expect("should find path");
assert!(path.len() >= 3, "path through 3 triangles should have at least 3 points");
assert_eq!(path[0], start);
assert_eq!(path[path.len() - 1], goal);
}
#[test]
fn test_path_outside_returns_none() {
let nm = make_strip();
// start is outside the mesh
let start = Vec3::new(100.0, 0.0, 100.0);
let goal = Vec3::new(0.8, 0.0, 0.5);
let result = find_path(&nm, start, goal);
assert!(result.is_none());
}
#[test]
fn test_path_disconnected_returns_none() {
let nm = make_disconnected();
let start = Vec3::new(0.8, 0.0, 0.5);
let goal = Vec3::new(11.0, 0.0, 10.5);
let result = find_path(&nm, start, goal);
assert!(result.is_none());
}
}

View File

@@ -0,0 +1,232 @@
use voltex_math::Vec3;
/// An agent that can be steered through the world.
#[derive(Debug, Clone)]
pub struct SteeringAgent {
pub position: Vec3,
pub velocity: Vec3,
pub max_speed: f32,
pub max_force: f32,
}
impl SteeringAgent {
pub fn new(position: Vec3, max_speed: f32, max_force: f32) -> Self {
Self {
position,
velocity: Vec3::ZERO,
max_speed,
max_force,
}
}
}
/// Truncate vector `v` to at most `max_len` magnitude.
pub fn truncate(v: Vec3, max_len: f32) -> Vec3 {
let len_sq = v.length_squared();
if len_sq > max_len * max_len {
v.normalize() * max_len
} else {
v
}
}
/// Seek steering behavior: move toward `target` at max speed.
/// Returns the steering force to apply.
pub fn seek(agent: &SteeringAgent, target: Vec3) -> Vec3 {
let to_target = target - agent.position;
let len = to_target.length();
if len < f32::EPSILON {
return Vec3::ZERO;
}
let desired = to_target.normalize() * agent.max_speed;
let steering = desired - agent.velocity;
truncate(steering, agent.max_force)
}
/// Flee steering behavior: move away from `threat` at max speed.
/// Returns the steering force to apply.
pub fn flee(agent: &SteeringAgent, threat: Vec3) -> Vec3 {
let away = agent.position - threat;
let len = away.length();
if len < f32::EPSILON {
return Vec3::ZERO;
}
let desired = away.normalize() * agent.max_speed;
let steering = desired - agent.velocity;
truncate(steering, agent.max_force)
}
/// Arrive steering behavior: decelerate when within `slow_radius` of `target`.
/// Returns the steering force to apply.
pub fn arrive(agent: &SteeringAgent, target: Vec3, slow_radius: f32) -> Vec3 {
let to_target = target - agent.position;
let dist = to_target.length();
if dist < f32::EPSILON {
return Vec3::ZERO;
}
let desired_speed = if dist < slow_radius {
agent.max_speed * (dist / slow_radius)
} else {
agent.max_speed
};
let desired = to_target.normalize() * desired_speed;
let steering = desired - agent.velocity;
truncate(steering, agent.max_force)
}
/// Wander steering behavior: steer toward a point on a circle projected ahead of the agent.
/// `wander_radius` is the radius of the wander circle,
/// `wander_distance` is how far ahead the circle is projected,
/// `angle` is the current angle on the wander circle (in radians).
/// Returns the steering force to apply.
pub fn wander(agent: &SteeringAgent, wander_radius: f32, wander_distance: f32, angle: f32) -> Vec3 {
// Compute forward direction; use +Z if velocity is near zero
let forward = {
let len = agent.velocity.length();
if len > f32::EPSILON {
agent.velocity.normalize()
} else {
Vec3::Z
}
};
// Circle center is ahead of the agent
let circle_center = agent.position + forward * wander_distance;
// Point on the circle at the given angle (in XZ plane)
let wander_target = Vec3::new(
circle_center.x + angle.cos() * wander_radius,
circle_center.y,
circle_center.z + angle.sin() * wander_radius,
);
seek(agent, wander_target)
}
/// Follow path steering behavior.
/// Seeks toward the current waypoint; advances to the next when within `waypoint_radius`.
/// Uses `arrive` for the last waypoint.
///
/// Returns (steering_force, updated_current_waypoint_index).
pub fn follow_path(
agent: &SteeringAgent,
path: &[Vec3],
current_waypoint: usize,
waypoint_radius: f32,
) -> (Vec3, usize) {
if path.is_empty() {
return (Vec3::ZERO, 0);
}
let clamped = current_waypoint.min(path.len() - 1);
let waypoint = path[clamped];
let dist = (waypoint - agent.position).length();
// If we are close enough and there is a next waypoint, advance
if dist < waypoint_radius && clamped + 1 < path.len() {
let next = clamped + 1;
let force = if next == path.len() - 1 {
arrive(agent, path[next], waypoint_radius)
} else {
seek(agent, path[next])
};
return (force, next);
}
// Use arrive for the last waypoint, seek for all others
let force = if clamped == path.len() - 1 {
arrive(agent, waypoint, waypoint_radius)
} else {
seek(agent, waypoint)
};
(force, clamped)
}
#[cfg(test)]
mod tests {
use super::*;
fn agent_at(x: f32, z: f32) -> SteeringAgent {
SteeringAgent::new(Vec3::new(x, 0.0, z), 5.0, 10.0)
}
#[test]
fn test_seek_direction() {
let agent = agent_at(0.0, 0.0);
let target = Vec3::new(1.0, 0.0, 0.0);
let force = seek(&agent, target);
// Force should be in the +X direction
assert!(force.x > 0.0, "seek force x should be positive");
assert!(force.z.abs() < 1e-4, "seek force z should be ~0");
}
#[test]
fn test_flee_direction() {
let agent = agent_at(0.0, 0.0);
let threat = Vec3::new(1.0, 0.0, 0.0);
let force = flee(&agent, threat);
// Force should be in the -X direction (away from threat)
assert!(force.x < 0.0, "flee force x should be negative");
assert!(force.z.abs() < 1e-4, "flee force z should be ~0");
}
#[test]
fn test_arrive_deceleration() {
let mut agent = agent_at(0.0, 0.0);
// Give agent some velocity toward target
agent.velocity = Vec3::new(5.0, 0.0, 0.0);
let target = Vec3::new(2.0, 0.0, 0.0); // within slow_radius=5
let slow_radius = 5.0;
let force = arrive(&agent, target, slow_radius);
// The desired speed is reduced (dist/slow_radius * max_speed = 2/5 * 5 = 2)
// desired velocity = (1,0,0)*2, current velocity = (5,0,0)
// steering = (2-5, 0, 0) = (-3, 0, 0) — a braking force
assert!(force.x < 0.0, "arrive should produce braking force when inside slow_radius");
}
#[test]
fn test_arrive_at_target() {
let agent = agent_at(0.0, 0.0);
let target = Vec3::new(0.0, 0.0, 0.0); // same position
let force = arrive(&agent, target, 1.0);
// At target, force should be zero
assert!(force.length() < 1e-4, "force at target should be zero");
}
#[test]
fn test_follow_path_advance() {
// Path: (0,0,0) -> (5,0,0) -> (10,0,0)
let path = vec![
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(5.0, 0.0, 0.0),
Vec3::new(10.0, 0.0, 0.0),
];
// Agent is very close to waypoint 1
let agent = SteeringAgent::new(Vec3::new(4.9, 0.0, 0.0), 5.0, 10.0);
let waypoint_radius = 0.5;
let (_, next_wp) = follow_path(&agent, &path, 1, waypoint_radius);
// Should advance to waypoint 2
assert_eq!(next_wp, 2, "should advance to waypoint 2 when close to waypoint 1");
}
#[test]
fn test_follow_path_last_arrives() {
// Path: single-segment, agent near last waypoint
let path = vec![
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(10.0, 0.0, 0.0),
];
let agent = SteeringAgent::new(Vec3::new(9.0, 0.0, 0.0), 5.0, 10.0);
let waypoint_radius = 2.0;
let (force, wp_idx) = follow_path(&agent, &path, 1, waypoint_radius);
// Still at waypoint 1 (the last one); force should be arrive (decelerating)
assert_eq!(wp_idx, 1);
// arrive should produce a deceleration toward (10,0,0)
// agent has zero velocity, dist=1, slow_radius=2 → desired_speed=1/2*5=2.5 in +X
// steering = desired - velocity = (2.5,0,0) - (0,0,0) = (2.5,0,0)
assert!(force.x > 0.0, "arrive force should be toward last waypoint");
}
}

View File

@@ -0,0 +1,7 @@
[package]
name = "voltex_audio"
version = "0.1.0"
edition = "2021"
[dependencies]
voltex_math.workspace = true

View File

@@ -0,0 +1,55 @@
/// A loaded audio clip stored as normalized f32 PCM samples.
#[derive(Debug)]
pub struct AudioClip {
/// Interleaved PCM samples in the range [-1.0, 1.0].
pub samples: Vec<f32>,
/// Number of samples per second (e.g. 44100).
pub sample_rate: u32,
/// Number of audio channels (1 = mono, 2 = stereo).
pub channels: u16,
}
impl AudioClip {
/// Create a new AudioClip from raw f32 samples.
pub fn new(samples: Vec<f32>, sample_rate: u32, channels: u16) -> Self {
Self {
samples,
sample_rate,
channels,
}
}
/// Total number of multi-channel frames.
/// A frame contains one sample per channel.
pub fn frame_count(&self) -> usize {
self.samples.len() / self.channels as usize
}
/// Duration in seconds.
pub fn duration(&self) -> f32 {
self.frame_count() as f32 / self.sample_rate as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mono_clip() {
// 44100 Hz, 1 channel, 1 second of silence
let samples = vec![0.0f32; 44100];
let clip = AudioClip::new(samples, 44100, 1);
assert_eq!(clip.frame_count(), 44100);
assert!((clip.duration() - 1.0).abs() < 1e-6);
}
#[test]
fn stereo_clip() {
// 44100 Hz, 2 channels, 0.5 seconds
let samples = vec![0.0f32; 44100]; // 44100 interleaved samples = 22050 frames
let clip = AudioClip::new(samples, 44100, 2);
assert_eq!(clip.frame_count(), 22050);
assert!((clip.duration() - 0.5).abs() < 1e-6);
}
}

View File

@@ -0,0 +1,303 @@
//! AudioSystem: high-level audio playback management with a dedicated audio thread.
use std::sync::{Arc, mpsc};
use std::thread;
use crate::{AudioClip, PlayingSound, mix_sounds};
use crate::spatial::{Listener, SpatialParams};
use crate::mix_group::{MixGroup, MixerState};
// ---------------------------------------------------------------------------
// AudioCommand
// ---------------------------------------------------------------------------
/// Commands sent to the audio thread.
pub enum AudioCommand {
/// Start playing a clip.
Play {
clip_index: usize,
volume: f32,
looping: bool,
},
/// Start playing a clip with 3D spatial parameters.
Play3d {
clip_index: usize,
volume: f32,
looping: bool,
spatial: SpatialParams,
},
/// Update the listener position and orientation.
SetListener {
position: voltex_math::Vec3,
forward: voltex_math::Vec3,
right: voltex_math::Vec3,
},
/// Stop all instances of a clip.
Stop { clip_index: usize },
/// Change volume of all playing instances of a clip.
SetVolume { clip_index: usize, volume: f32 },
/// Stop all currently playing sounds.
StopAll,
/// Set volume for a mix group immediately.
SetGroupVolume { group: MixGroup, volume: f32 },
/// Fade a mix group to a target volume over a duration (seconds).
FadeGroup { group: MixGroup, target: f32, duration: f32 },
/// Shut down the audio thread.
Shutdown,
}
// ---------------------------------------------------------------------------
// AudioSystem
// ---------------------------------------------------------------------------
/// Manages audio playback via a dedicated OS-level thread.
pub struct AudioSystem {
sender: mpsc::Sender<AudioCommand>,
}
impl AudioSystem {
/// Create an AudioSystem with the given pre-loaded clips.
///
/// Spawns a background audio thread that owns the clips and drives the
/// WASAPI device (on Windows) or runs in silent mode (other platforms).
pub fn new(clips: Vec<AudioClip>) -> Result<Self, String> {
let clips = Arc::new(clips);
let (tx, rx) = mpsc::channel::<AudioCommand>();
let clips_arc = Arc::clone(&clips);
thread::Builder::new()
.name("voltex_audio_thread".to_string())
.spawn(move || {
audio_thread(rx, clips_arc);
})
.map_err(|e| format!("Failed to spawn audio thread: {}", e))?;
Ok(AudioSystem { sender: tx })
}
/// Start playing a clip at the given volume and looping setting.
pub fn play(&self, clip_index: usize, volume: f32, looping: bool) {
let _ = self.sender.send(AudioCommand::Play {
clip_index,
volume,
looping,
});
}
/// Start playing a clip with 3D spatial audio parameters.
pub fn play_3d(&self, clip_index: usize, volume: f32, looping: bool, spatial: SpatialParams) {
let _ = self.sender.send(AudioCommand::Play3d { clip_index, volume, looping, spatial });
}
/// Update the listener position and orientation for 3D audio.
pub fn set_listener(&self, position: voltex_math::Vec3, forward: voltex_math::Vec3, right: voltex_math::Vec3) {
let _ = self.sender.send(AudioCommand::SetListener { position, forward, right });
}
/// Stop all instances of the specified clip.
pub fn stop(&self, clip_index: usize) {
let _ = self.sender.send(AudioCommand::Stop { clip_index });
}
/// Set the volume for all playing instances of a clip.
pub fn set_volume(&self, clip_index: usize, volume: f32) {
let _ = self.sender.send(AudioCommand::SetVolume { clip_index, volume });
}
/// Stop all currently playing sounds.
pub fn stop_all(&self) {
let _ = self.sender.send(AudioCommand::StopAll);
}
/// Set volume for a mix group immediately.
pub fn set_group_volume(&self, group: MixGroup, volume: f32) {
let _ = self.sender.send(AudioCommand::SetGroupVolume { group, volume });
}
/// Fade a mix group to a target volume over `duration` seconds.
pub fn fade_group(&self, group: MixGroup, target: f32, duration: f32) {
let _ = self.sender.send(AudioCommand::FadeGroup { group, target, duration });
}
}
impl Drop for AudioSystem {
fn drop(&mut self) {
let _ = self.sender.send(AudioCommand::Shutdown);
}
}
// ---------------------------------------------------------------------------
// Audio thread implementation
// ---------------------------------------------------------------------------
/// Main audio thread function.
///
/// On Windows, initializes WasapiDevice and drives playback.
/// On other platforms, runs in a silent "null" mode (processes commands only).
fn audio_thread(rx: mpsc::Receiver<AudioCommand>, clips: Arc<Vec<AudioClip>>) {
#[cfg(target_os = "windows")]
audio_thread_windows(rx, clips);
#[cfg(not(target_os = "windows"))]
audio_thread_null(rx, clips);
}
// ---------------------------------------------------------------------------
// Windows implementation
// ---------------------------------------------------------------------------
#[cfg(target_os = "windows")]
fn audio_thread_windows(rx: mpsc::Receiver<AudioCommand>, clips: Arc<Vec<AudioClip>>) {
use crate::wasapi::WasapiDevice;
let device = match WasapiDevice::new() {
Ok(d) => d,
Err(e) => {
eprintln!("[voltex_audio] WASAPI init failed: {}. Running in silent mode.", e);
audio_thread_null(rx, clips);
return;
}
};
let device_sample_rate = device.sample_rate;
let device_channels = device.channels;
let buffer_frames = device.buffer_frames() as usize;
let mut playing: Vec<PlayingSound> = Vec::new();
let mut output: Vec<f32> = Vec::new();
let mut listener = Listener::default();
let mut mixer = MixerState::new();
loop {
// Advance mixer fades (~5 ms per iteration)
mixer.tick(0.005);
// Process all pending commands (non-blocking)
loop {
match rx.try_recv() {
Ok(cmd) => {
match cmd {
AudioCommand::Play { clip_index, volume, looping } => {
playing.push(PlayingSound::new(clip_index, volume, looping));
}
AudioCommand::Play3d { clip_index, volume, looping, spatial } => {
playing.push(PlayingSound::new_3d(clip_index, volume, looping, spatial));
}
AudioCommand::SetListener { position, forward, right } => {
listener = Listener { position, forward, right };
}
AudioCommand::Stop { clip_index } => {
playing.retain(|s| s.clip_index != clip_index);
}
AudioCommand::SetVolume { clip_index, volume } => {
for s in playing.iter_mut() {
if s.clip_index == clip_index {
s.volume = volume;
}
}
}
AudioCommand::StopAll => {
playing.clear();
}
AudioCommand::SetGroupVolume { group, volume } => {
mixer.set_volume(group, volume);
}
AudioCommand::FadeGroup { group, target, duration } => {
mixer.fade(group, target, duration);
}
AudioCommand::Shutdown => {
return;
}
}
}
Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => return,
}
}
// Mix and write audio
if !playing.is_empty() || !output.is_empty() {
mix_sounds(
&mut output,
&mut playing,
&clips,
device_sample_rate,
device_channels,
buffer_frames,
&listener,
&mixer,
);
if let Err(e) = device.write_samples(&output) {
eprintln!("[voltex_audio] write_samples error: {}", e);
}
} else {
// Write silence to keep the device happy
let silence_len = buffer_frames * device_channels as usize;
let silence = vec![0.0f32; silence_len];
let _ = device.write_samples(&silence);
}
// Sleep ~5 ms between iterations
thread::sleep(std::time::Duration::from_millis(5));
}
}
// ---------------------------------------------------------------------------
// Null (non-Windows) implementation
// ---------------------------------------------------------------------------
#[cfg(not(target_os = "windows"))]
fn audio_thread_null(rx: mpsc::Receiver<AudioCommand>, clips: Arc<Vec<AudioClip>>) {
audio_thread_null_impl(rx, clips);
}
#[cfg(target_os = "windows")]
fn audio_thread_null(rx: mpsc::Receiver<AudioCommand>, _clips: Arc<Vec<AudioClip>>) {
audio_thread_null_impl(rx, _clips);
}
fn audio_thread_null_impl(rx: mpsc::Receiver<AudioCommand>, _clips: Arc<Vec<AudioClip>>) {
loop {
match rx.recv() {
Ok(AudioCommand::Shutdown) | Err(_) => return,
Ok(_) => {}
}
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::AudioClip;
fn silence_clip() -> AudioClip {
AudioClip::new(vec![0.0f32; 44100], 44100, 1)
}
#[test]
fn create_and_drop() {
// AudioSystem should be created and dropped without panicking.
// The audio thread is spawned but will run in null mode in CI.
let _sys = AudioSystem::new(vec![silence_clip()])
.expect("AudioSystem::new failed");
// Drop happens here, Shutdown command is sent automatically.
}
#[test]
fn send_commands() {
let sys = AudioSystem::new(vec![silence_clip(), silence_clip()])
.expect("AudioSystem::new failed");
sys.play(0, 1.0, false);
sys.play(1, 0.5, true);
sys.set_volume(0, 0.8);
sys.stop(0);
sys.stop_all();
// All sends must succeed without panic.
}
}

View File

@@ -0,0 +1,15 @@
pub mod audio_clip;
pub mod wav;
pub mod mixing;
#[cfg(target_os = "windows")]
pub mod wasapi;
pub mod audio_system;
pub mod spatial;
pub mod mix_group;
pub use audio_clip::AudioClip;
pub use wav::{parse_wav, generate_wav_bytes};
pub use mixing::{PlayingSound, mix_sounds};
pub use audio_system::AudioSystem;
pub use spatial::{Listener, SpatialParams, distance_attenuation, stereo_pan, compute_spatial_gains};
pub use mix_group::{MixGroup, MixerState};

View File

@@ -0,0 +1,214 @@
/// Mixer group identifiers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MixGroup {
Master = 0,
Bgm = 1,
Sfx = 2,
Voice = 3,
}
/// Per-group volume state with optional fade.
#[derive(Debug, Clone)]
pub struct GroupState {
pub volume: f32,
pub fade_target: f32,
pub fade_speed: f32,
}
impl GroupState {
pub fn new() -> Self {
Self {
volume: 1.0,
fade_target: 1.0,
fade_speed: 0.0,
}
}
/// Advance fade by `dt` seconds.
pub fn tick(&mut self, dt: f32) {
if self.fade_speed == 0.0 {
return;
}
let delta = self.fade_speed * dt;
if self.volume < self.fade_target {
self.volume = (self.volume + delta).min(self.fade_target);
} else {
self.volume = (self.volume - delta).max(self.fade_target);
}
if (self.volume - self.fade_target).abs() < f32::EPSILON {
self.volume = self.fade_target;
self.fade_speed = 0.0;
}
}
}
impl Default for GroupState {
fn default() -> Self {
Self::new()
}
}
/// Global mixer state holding one `GroupState` per `MixGroup`.
pub struct MixerState {
pub groups: [GroupState; 4],
}
impl MixerState {
pub fn new() -> Self {
Self {
groups: [
GroupState::new(),
GroupState::new(),
GroupState::new(),
GroupState::new(),
],
}
}
fn idx(group: MixGroup) -> usize {
group as usize
}
/// Immediately set volume for `group`, cancelling any active fade.
pub fn set_volume(&mut self, group: MixGroup, volume: f32) {
let v = volume.clamp(0.0, 1.0);
let g = &mut self.groups[Self::idx(group)];
g.volume = v;
g.fade_target = v;
g.fade_speed = 0.0;
}
/// Begin a fade from the current volume to `target` over `duration` seconds.
/// If `duration` <= 0, set volume instantly.
pub fn fade(&mut self, group: MixGroup, target: f32, duration: f32) {
let target = target.clamp(0.0, 1.0);
let g = &mut self.groups[Self::idx(group)];
if duration <= 0.0 {
g.volume = target;
g.fade_target = target;
g.fade_speed = 0.0;
} else {
let speed = (target - g.volume).abs() / duration;
g.fade_target = target;
g.fade_speed = speed;
}
}
/// Advance all groups by `dt` seconds.
pub fn tick(&mut self, dt: f32) {
for g in &mut self.groups {
g.tick(dt);
}
}
/// Current volume of `group`.
pub fn volume(&self, group: MixGroup) -> f32 {
self.groups[Self::idx(group)].volume
}
/// Effective volume: group volume multiplied by master volume.
/// For `Master`, returns its own volume only.
pub fn effective_volume(&self, group: MixGroup) -> f32 {
let master = self.groups[Self::idx(MixGroup::Master)].volume;
if group == MixGroup::Master {
master
} else {
self.groups[Self::idx(group)].volume * master
}
}
}
impl Default for MixerState {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn group_state_tick_fade() {
let mut g = GroupState::new();
g.volume = 1.0;
g.fade_target = 0.0;
g.fade_speed = 2.0;
g.tick(0.25); // 0.25 * 2.0 = 0.5 moved
assert!((g.volume - 0.5).abs() < 1e-5, "expected ~0.5, got {}", g.volume);
g.tick(0.25); // another 0.5 → reaches 0.0
assert!((g.volume - 0.0).abs() < 1e-5, "expected 0.0, got {}", g.volume);
assert_eq!(g.fade_speed, 0.0, "fade_speed should be 0 after reaching target");
}
#[test]
fn group_state_tick_no_overshoot() {
let mut g = GroupState::new();
g.volume = 1.0;
g.fade_target = 0.0;
g.fade_speed = 100.0; // very fast
g.tick(1.0);
assert_eq!(g.volume, 0.0, "should not overshoot below 0");
assert_eq!(g.fade_speed, 0.0);
}
#[test]
fn mixer_set_volume() {
let mut m = MixerState::new();
// Start a fade, then override with set_volume
m.fade(MixGroup::Sfx, 0.0, 5.0);
m.set_volume(MixGroup::Sfx, 0.3);
assert!((m.volume(MixGroup::Sfx) - 0.3).abs() < 1e-5);
assert_eq!(m.groups[MixGroup::Sfx as usize].fade_speed, 0.0, "fade cancelled");
}
#[test]
fn mixer_fade() {
let mut m = MixerState::new();
m.fade(MixGroup::Sfx, 0.0, 1.0); // 1→0 over 1s, speed=1.0
m.tick(0.5);
let v = m.volume(MixGroup::Sfx);
assert!((v - 0.5).abs() < 1e-5, "at 0.5s expected ~0.5, got {}", v);
m.tick(0.5);
let v = m.volume(MixGroup::Sfx);
assert!((v - 0.0).abs() < 1e-5, "at 1.0s expected 0.0, got {}", v);
}
#[test]
fn effective_volume() {
let mut m = MixerState::new();
m.set_volume(MixGroup::Master, 0.5);
m.set_volume(MixGroup::Sfx, 0.8);
let ev = m.effective_volume(MixGroup::Sfx);
assert!((ev - 0.4).abs() < 1e-5, "expected 0.4, got {}", ev);
}
#[test]
fn master_zero_mutes_all() {
let mut m = MixerState::new();
m.set_volume(MixGroup::Master, 0.0);
for group in [MixGroup::Bgm, MixGroup::Sfx, MixGroup::Voice] {
assert_eq!(m.effective_volume(group), 0.0);
}
}
#[test]
fn fade_up() {
let mut m = MixerState::new();
m.set_volume(MixGroup::Bgm, 0.0);
m.fade(MixGroup::Bgm, 1.0, 2.0); // 0→1 over 2s, speed=0.5
m.tick(1.0);
let v = m.volume(MixGroup::Bgm);
assert!((v - 0.5).abs() < 1e-5, "at 1s expected ~0.5, got {}", v);
m.tick(1.0);
let v = m.volume(MixGroup::Bgm);
assert!((v - 1.0).abs() < 1e-5, "at 2s expected 1.0, got {}", v);
}
}

View File

@@ -0,0 +1,373 @@
use crate::AudioClip;
use crate::spatial::{Listener, SpatialParams, compute_spatial_gains};
use crate::mix_group::{MixGroup, MixerState};
/// Represents a sound currently being played back.
pub struct PlayingSound {
/// Index into the clips slice passed to `mix_sounds`.
pub clip_index: usize,
/// Current playback position in frames (not samples).
pub position: usize,
/// Linear volume multiplier [0.0 = silent, 1.0 = full].
pub volume: f32,
/// Whether the sound loops when it reaches the end.
pub looping: bool,
/// Optional 3D spatial parameters. None = 2D (no spatialization).
pub spatial: Option<SpatialParams>,
/// Mix group this sound belongs to.
pub group: MixGroup,
}
impl PlayingSound {
pub fn new(clip_index: usize, volume: f32, looping: bool) -> Self {
Self {
clip_index,
position: 0,
volume,
looping,
spatial: None,
group: MixGroup::Sfx,
}
}
pub fn new_3d(clip_index: usize, volume: f32, looping: bool, spatial: SpatialParams) -> Self {
Self { clip_index, position: 0, volume, looping, spatial: Some(spatial), group: MixGroup::Sfx }
}
}
/// Mix all active sounds in `playing` into `output`.
///
/// # Parameters
/// - `output`: interleaved output buffer (len = frames * device_channels).
/// - `playing`: mutable list of active sounds; finished non-looping sounds are removed.
/// - `clips`: the audio clip assets.
/// - `device_sample_rate`: output device sample rate.
/// - `device_channels`: output device channel count (1 or 2).
/// - `frames`: number of output frames to fill.
/// - `listener`: the listener for 3D spatialization.
/// - `mixer`: the global mixer state providing per-group and master volumes.
pub fn mix_sounds(
output: &mut Vec<f32>,
playing: &mut Vec<PlayingSound>,
clips: &[AudioClip],
device_sample_rate: u32,
device_channels: u16,
frames: usize,
listener: &Listener,
mixer: &MixerState,
) {
// Ensure output buffer is sized correctly and zeroed.
let output_len = frames * device_channels as usize;
output.clear();
output.resize(output_len, 0.0);
let mut finished = Vec::new();
for (sound_idx, sound) in playing.iter_mut().enumerate() {
let clip = &clips[sound.clip_index];
// Apply group and master volume multiplier.
let group_vol = mixer.effective_volume(sound.group);
let base_volume = sound.volume * group_vol;
// Compute per-channel effective volumes based on spatial params.
let (vol_left, vol_right) = if let Some(ref spatial) = sound.spatial {
let (atten, left_gain, right_gain) = compute_spatial_gains(listener, spatial);
(base_volume * atten * left_gain, base_volume * atten * right_gain)
} else {
(base_volume, base_volume)
};
// Compute integer rate ratio for naive resampling.
// For same-rate clips ratio == 1, so we read every frame.
let rate_ratio = if device_sample_rate > 0 {
clip.sample_rate as f64 / device_sample_rate as f64
} else {
1.0
};
for frame_out in 0..frames {
// Map the output frame index to a clip frame index.
let clip_frame = sound.position + (frame_out as f64 * rate_ratio) as usize;
if clip_frame >= clip.frame_count() {
// Sound exhausted within this render block.
if sound.looping {
// We'll reset after the loop; for now just stop filling.
}
break;
}
// Fetch sample(s) from clip frame.
let out_base = frame_out * device_channels as usize;
if device_channels == 2 {
let (left, right) = if clip.channels == 1 {
// Mono -> stereo: apply per-channel volumes
let raw = clip.samples[clip_frame];
(raw * vol_left, raw * vol_right)
} else {
// Stereo clip
let l = clip.samples[clip_frame * 2] * vol_left;
let r = clip.samples[clip_frame * 2 + 1] * vol_right;
(l, r)
};
output[out_base] += left;
output[out_base + 1] += right;
} else {
// Mono output: average left and right volumes
let vol_mono = (vol_left + vol_right) * 0.5;
let s = if clip.channels == 1 {
clip.samples[clip_frame] * vol_mono
} else {
// Mix stereo clip to mono
(clip.samples[clip_frame * 2] + clip.samples[clip_frame * 2 + 1])
* 0.5
* vol_mono
};
output[out_base] += s;
}
}
// Advance position by the number of clip frames consumed this block.
let frames_consumed = (frames as f64 * rate_ratio) as usize;
sound.position += frames_consumed;
if sound.position >= clip.frame_count() {
if sound.looping {
sound.position = 0;
} else {
finished.push(sound_idx);
}
}
}
// Remove finished sounds in reverse order to preserve indices.
for &idx in finished.iter().rev() {
playing.remove(idx);
}
// Clamp output to [-1.0, 1.0].
for sample in output.iter_mut() {
*sample = sample.clamp(-1.0, 1.0);
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::AudioClip;
use crate::spatial::{Listener, SpatialParams};
use crate::mix_group::{MixGroup, MixerState};
use voltex_math::Vec3;
fn make_mono_clip(value: f32, frames: usize, sample_rate: u32) -> AudioClip {
AudioClip::new(vec![value; frames], sample_rate, 1)
}
fn make_stereo_clip(left: f32, right: f32, frames: usize, sample_rate: u32) -> AudioClip {
let mut samples = Vec::with_capacity(frames * 2);
for _ in 0..frames {
samples.push(left);
samples.push(right);
}
AudioClip::new(samples, sample_rate, 2)
}
#[test]
fn single_sound_volume() {
let clips = vec![make_mono_clip(1.0, 100, 44100)];
let mut playing = vec![PlayingSound::new(0, 0.5, false)];
let mut output = Vec::new();
mix_sounds(&mut output, &mut playing, &clips, 44100, 1, 10, &Listener::default(), &MixerState::new());
// All output frames should be 0.5 (1.0 * 0.5 volume)
assert_eq!(output.len(), 10);
for &s in &output {
assert!((s - 0.5).abs() < 1e-6, "expected 0.5, got {}", s);
}
}
#[test]
fn two_sounds_sum() {
// Two clips each at 0.3; sum is 0.6 (below clamp threshold)
let clips = vec![
make_mono_clip(0.3, 100, 44100),
make_mono_clip(0.3, 100, 44100),
];
let mut playing = vec![
PlayingSound::new(0, 1.0, false),
PlayingSound::new(1, 1.0, false),
];
let mut output = Vec::new();
mix_sounds(&mut output, &mut playing, &clips, 44100, 1, 10, &Listener::default(), &MixerState::new());
assert_eq!(output.len(), 10);
for &s in &output {
assert!((s - 0.6).abs() < 1e-5, "expected 0.6, got {}", s);
}
}
#[test]
fn clipping() {
// Two clips at 1.0 each; sum would be 2.0 but must be clamped to 1.0
let clips = vec![
make_mono_clip(1.0, 100, 44100),
make_mono_clip(1.0, 100, 44100),
];
let mut playing = vec![
PlayingSound::new(0, 1.0, false),
PlayingSound::new(1, 1.0, false),
];
let mut output = Vec::new();
mix_sounds(&mut output, &mut playing, &clips, 44100, 1, 10, &Listener::default(), &MixerState::new());
for &s in &output {
assert!(s <= 1.0, "output {} exceeds 1.0 (clamp failed)", s);
assert!(s >= -1.0, "output {} below -1.0 (clamp failed)", s);
}
}
#[test]
fn non_looping_removal() {
// Clip is only 5 frames; request 20 frames; sound should be removed after
let clips = vec![make_mono_clip(0.5, 5, 44100)];
let mut playing = vec![PlayingSound::new(0, 1.0, false)];
let mut output = Vec::new();
mix_sounds(&mut output, &mut playing, &clips, 44100, 1, 20, &Listener::default(), &MixerState::new());
// Sound should have been removed
assert!(playing.is_empty(), "non-looping sound was not removed");
}
#[test]
fn looping_continues() {
// Clip is 5 frames; request 20 frames; looping sound should remain
let clips = vec![make_mono_clip(0.5, 5, 44100)];
let mut playing = vec![PlayingSound::new(0, 1.0, true)];
let mut output = Vec::new();
mix_sounds(&mut output, &mut playing, &clips, 44100, 1, 20, &Listener::default(), &MixerState::new());
// Sound should still be in the list
assert_eq!(playing.len(), 1, "looping sound was incorrectly removed");
// Position should have been reset to 0
assert_eq!(playing[0].position, 0);
}
#[test]
fn mono_to_stereo() {
// Mono clip mixed to stereo output: both channels should have same value
let clips = vec![make_mono_clip(0.7, 100, 44100)];
let mut playing = vec![PlayingSound::new(0, 1.0, false)];
let mut output = Vec::new();
mix_sounds(&mut output, &mut playing, &clips, 44100, 2, 10, &Listener::default(), &MixerState::new());
// output length = 10 frames * 2 channels = 20
assert_eq!(output.len(), 20);
for i in 0..10 {
let l = output[i * 2];
let r = output[i * 2 + 1];
assert!((l - 0.7).abs() < 1e-6, "left={}", l);
assert!((r - 0.7).abs() < 1e-6, "right={}", r);
}
}
// -----------------------------------------------------------------------
// Spatial audio tests
// -----------------------------------------------------------------------
#[test]
fn spatial_2d_unchanged() {
// spatial=None should produce the same output as before (no spatialization)
let clips = vec![make_mono_clip(1.0, 100, 44100)];
let mut playing = vec![PlayingSound::new(0, 0.5, false)];
let mut output = Vec::new();
mix_sounds(&mut output, &mut playing, &clips, 44100, 1, 10, &Listener::default(), &MixerState::new());
assert_eq!(output.len(), 10);
for &s in &output {
assert!((s - 0.5).abs() < 1e-6, "expected 0.5, got {}", s);
}
}
#[test]
fn spatial_far_away_silent() {
// Emitter beyond max_distance → attenuation = 0.0 → ~0 output
let clips = vec![make_mono_clip(1.0, 100, 44100)];
let spatial = SpatialParams::new(Vec3::new(200.0, 0.0, 0.0), 1.0, 100.0);
let mut playing = vec![PlayingSound::new_3d(0, 1.0, false, spatial)];
let mut output = Vec::new();
mix_sounds(&mut output, &mut playing, &clips, 44100, 2, 10, &Listener::default(), &MixerState::new());
assert_eq!(output.len(), 20);
for &s in &output {
assert!(s.abs() < 1e-6, "expected ~0 for far-away emitter, got {}", s);
}
}
#[test]
fn spatial_right_panning() {
// Emitter on +X → right channel should be louder than left channel (stereo output)
let clips = vec![make_mono_clip(1.0, 100, 44100)];
// Emitter close enough to be audible (within min_distance → atten=1)
let spatial = SpatialParams::new(Vec3::new(0.5, 0.0, 0.0), 1.0, 100.0);
let mut playing = vec![PlayingSound::new_3d(0, 1.0, false, spatial)];
let mut output = Vec::new();
mix_sounds(&mut output, &mut playing, &clips, 44100, 2, 10, &Listener::default(), &MixerState::new());
assert_eq!(output.len(), 20);
// Check first frame: left=output[0], right=output[1]
let left = output[0];
let right = output[1];
assert!(right > left, "expected right > left for +X emitter, got left={}, right={}", left, right);
}
#[test]
fn group_volume_applied() {
// Sfx group at 0.5: output should be halved compared to default (1.0)
let clips = vec![make_mono_clip(1.0, 100, 44100)];
let mut playing = vec![PlayingSound::new(0, 1.0, false)];
let mut output = Vec::new();
let mut mixer = MixerState::new();
mixer.set_volume(MixGroup::Sfx, 0.5);
mix_sounds(&mut output, &mut playing, &clips, 44100, 1, 10, &Listener::default(), &mixer);
assert_eq!(output.len(), 10);
for &s in &output {
assert!((s - 0.5).abs() < 1e-6, "expected 0.5 (halved by Sfx group vol), got {}", s);
}
}
#[test]
fn master_zero_mutes_output() {
// Master volume at 0: all output must be silence
let clips = vec![make_mono_clip(1.0, 100, 44100)];
let mut playing = vec![PlayingSound::new(0, 1.0, false)];
let mut output = Vec::new();
let mut mixer = MixerState::new();
mixer.set_volume(MixGroup::Master, 0.0);
mix_sounds(&mut output, &mut playing, &clips, 44100, 1, 10, &Listener::default(), &mixer);
assert_eq!(output.len(), 10);
for &s in &output {
assert!(s.abs() < 1e-6, "expected silence with master=0, got {}", s);
}
}
}

View File

@@ -0,0 +1,162 @@
use voltex_math::Vec3;
use std::f32::consts::PI;
/// Represents the listener in 3D space.
#[derive(Debug, Clone)]
pub struct Listener {
pub position: Vec3,
pub forward: Vec3,
pub right: Vec3,
}
impl Default for Listener {
fn default() -> Self {
Self {
position: Vec3::ZERO,
forward: Vec3::new(0.0, 0.0, -1.0),
right: Vec3::X,
}
}
}
/// Parameters for a 3D audio emitter.
#[derive(Debug, Clone)]
pub struct SpatialParams {
pub position: Vec3,
pub min_distance: f32,
pub max_distance: f32,
}
impl SpatialParams {
/// Create with explicit parameters.
pub fn new(position: Vec3, min_distance: f32, max_distance: f32) -> Self {
Self { position, min_distance, max_distance }
}
/// Create at a position with default distances (1.0 min, 100.0 max).
pub fn at(position: Vec3) -> Self {
Self { position, min_distance: 1.0, max_distance: 100.0 }
}
}
/// Inverse-distance attenuation model.
/// Returns 1.0 when distance <= min_dist, 0.0 when distance >= max_dist,
/// otherwise min_dist / distance clamped to [0, 1].
pub fn distance_attenuation(distance: f32, min_dist: f32, max_dist: f32) -> f32 {
if distance <= min_dist {
1.0
} else if distance >= max_dist {
0.0
} else {
(min_dist / distance).clamp(0.0, 1.0)
}
}
/// Equal-power stereo panning based on listener orientation and emitter position.
/// Returns (left_gain, right_gain).
/// If the emitter is at the same position as the listener, returns (1.0, 1.0).
pub fn stereo_pan(listener: &Listener, emitter_pos: Vec3) -> (f32, f32) {
let diff = emitter_pos - listener.position;
let distance = diff.length();
const EPSILON: f32 = 1e-6;
if distance < EPSILON {
return (1.0, 1.0);
}
let direction = diff * (1.0 / distance);
let pan = direction.dot(listener.right).clamp(-1.0, 1.0);
// Map pan [-1, 1] to angle [0, PI/2] via angle = pan * PI/4 + PI/4
let angle = pan * (PI / 4.0) + (PI / 4.0);
let left = angle.cos();
let right = angle.sin();
(left, right)
}
/// Convenience function combining distance attenuation and stereo panning.
/// Returns (attenuation, left_gain, right_gain).
pub fn compute_spatial_gains(listener: &Listener, spatial: &SpatialParams) -> (f32, f32, f32) {
let diff = spatial.position - listener.position;
let distance = diff.length();
let attenuation = distance_attenuation(distance, spatial.min_distance, spatial.max_distance);
let (left, right) = stereo_pan(listener, spatial.position);
(attenuation, left, right)
}
#[cfg(test)]
mod tests {
use super::*;
use voltex_math::Vec3;
#[test]
fn attenuation_at_min() {
// distance <= min_distance should return 1.0
assert_eq!(distance_attenuation(0.5, 1.0, 10.0), 1.0);
assert_eq!(distance_attenuation(1.0, 1.0, 10.0), 1.0);
}
#[test]
fn attenuation_at_max() {
// distance >= max_distance should return 0.0
assert_eq!(distance_attenuation(10.0, 1.0, 10.0), 0.0);
assert_eq!(distance_attenuation(50.0, 1.0, 10.0), 0.0);
}
#[test]
fn attenuation_between() {
// inverse: min_dist / distance
let result = distance_attenuation(5.0, 1.0, 10.0);
let expected = 1.0_f32 / 5.0_f32;
assert!((result - expected).abs() < 1e-6, "expected {expected}, got {result}");
}
#[test]
fn pan_right() {
// Emitter to the right (+X) should give right > left
let listener = Listener::default();
let emitter = Vec3::new(10.0, 0.0, 0.0);
let (left, right) = stereo_pan(&listener, emitter);
assert!(right > left, "expected right > left for +X emitter, got left={left}, right={right}");
}
#[test]
fn pan_left() {
// Emitter to the left (-X) should give left > right
let listener = Listener::default();
let emitter = Vec3::new(-10.0, 0.0, 0.0);
let (left, right) = stereo_pan(&listener, emitter);
assert!(left > right, "expected left > right for -X emitter, got left={left}, right={right}");
}
#[test]
fn pan_front() {
// Emitter directly in front (-Z) should give roughly equal gains
let listener = Listener::default();
let emitter = Vec3::new(0.0, 0.0, -10.0);
let (left, right) = stereo_pan(&listener, emitter);
assert!((left - right).abs() < 0.01, "expected roughly equal for front emitter, got left={left}, right={right}");
}
#[test]
fn pan_same_position() {
// Same position as listener should return (1.0, 1.0)
let listener = Listener::default();
let emitter = Vec3::ZERO;
let (left, right) = stereo_pan(&listener, emitter);
assert_eq!((left, right), (1.0, 1.0));
}
#[test]
fn compute_spatial_gains_combines_both() {
let listener = Listener::default();
// Emitter at (5, 0, 0) with min=1, max=10
let spatial = SpatialParams::new(Vec3::new(5.0, 0.0, 0.0), 1.0, 10.0);
let (attenuation, left, right) = compute_spatial_gains(&listener, &spatial);
// Check attenuation: distance=5, min=1, max=10 → 1/5 = 0.2
let expected_atten = 1.0_f32 / 5.0_f32;
assert!((attenuation - expected_atten).abs() < 1e-6,
"attenuation mismatch: expected {expected_atten}, got {attenuation}");
// Emitter is to the right, so right > left
assert!(right > left, "expected right > left, got left={left}, right={right}");
}
}

View File

@@ -0,0 +1,521 @@
#![allow(non_snake_case, non_camel_case_types, dead_code)]
//! WASAPI FFI bindings for Windows audio output.
//!
//! This module is only compiled on Windows (`#[cfg(target_os = "windows")]`).
use std::ffi::c_void;
// ---------------------------------------------------------------------------
// Basic Windows types
// ---------------------------------------------------------------------------
pub type HRESULT = i32;
pub type ULONG = u32;
pub type DWORD = u32;
pub type WORD = u16;
pub type BOOL = i32;
pub type UINT = u32;
pub type UINT32 = u32;
pub type BYTE = u8;
pub type REFERENCE_TIME = i64;
pub type HANDLE = *mut c_void;
pub type LPVOID = *mut c_void;
pub type LPCWSTR = *const u16;
pub const S_OK: HRESULT = 0;
pub const AUDCLNT_SHAREMODE_SHARED: u32 = 0;
pub const AUDCLNT_STREAMFLAGS_RATEADJUST: u32 = 0x00100000;
pub const CLSCTX_ALL: DWORD = 0x17;
pub const COINIT_APARTMENTTHREADED: DWORD = 0x2;
pub const DEVICE_STATE_ACTIVE: DWORD = 0x1;
pub const eRender: u32 = 0;
pub const eConsole: u32 = 0;
pub const WAVE_FORMAT_PCM: WORD = 1;
pub const WAVE_FORMAT_IEEE_FLOAT: WORD = 3;
pub const WAVE_FORMAT_EXTENSIBLE: WORD = 0xFFFE;
// ---------------------------------------------------------------------------
// GUID
// ---------------------------------------------------------------------------
#[repr(C)]
#[derive(Clone, Copy)]
pub struct GUID {
pub Data1: u32,
pub Data2: u16,
pub Data3: u16,
pub Data4: [u8; 8],
}
/// CLSID_MMDeviceEnumerator: {BCDE0395-E52F-467C-8E3D-C4579291692E}
pub const CLSID_MMDeviceEnumerator: GUID = GUID {
Data1: 0xBCDE0395,
Data2: 0xE52F,
Data3: 0x467C,
Data4: [0x8E, 0x3D, 0xC4, 0x57, 0x92, 0x91, 0x69, 0x2E],
};
/// IID_IMMDeviceEnumerator: {A95664D2-9614-4F35-A746-DE8DB63617E6}
pub const IID_IMMDeviceEnumerator: GUID = GUID {
Data1: 0xA95664D2,
Data2: 0x9614,
Data3: 0x4F35,
Data4: [0xA7, 0x46, 0xDE, 0x8D, 0xB6, 0x36, 0x17, 0xE6],
};
/// IID_IAudioClient: {1CB9AD4C-DBFA-4c32-B178-C2F568A703B2}
pub const IID_IAudioClient: GUID = GUID {
Data1: 0x1CB9AD4C,
Data2: 0xDBFA,
Data3: 0x4C32,
Data4: [0xB1, 0x78, 0xC2, 0xF5, 0x68, 0xA7, 0x03, 0xB2],
};
/// IID_IAudioRenderClient: {F294ACFC-3146-4483-A7BF-ADDCA7C260E2}
pub const IID_IAudioRenderClient: GUID = GUID {
Data1: 0xF294ACFC,
Data2: 0x3146,
Data3: 0x4483,
Data4: [0xA7, 0xBF, 0xAD, 0xDC, 0xA7, 0xC2, 0x60, 0xE2],
};
// ---------------------------------------------------------------------------
// WAVEFORMATEX
// ---------------------------------------------------------------------------
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WAVEFORMATEX {
pub wFormatTag: WORD,
pub nChannels: WORD,
pub nSamplesPerSec: DWORD,
pub nAvgBytesPerSec: DWORD,
pub nBlockAlign: WORD,
pub wBitsPerSample: WORD,
pub cbSize: WORD,
}
// ---------------------------------------------------------------------------
// COM vtable structs
// ---------------------------------------------------------------------------
/// IUnknown vtable (base for all COM interfaces)
#[repr(C)]
pub struct IUnknownVtbl {
pub QueryInterface: unsafe extern "system" fn(
this: *mut c_void,
riid: *const GUID,
ppvObject: *mut *mut c_void,
) -> HRESULT,
pub AddRef: unsafe extern "system" fn(this: *mut c_void) -> ULONG,
pub Release: unsafe extern "system" fn(this: *mut c_void) -> ULONG,
}
/// IMMDeviceEnumerator vtable
#[repr(C)]
pub struct IMMDeviceEnumeratorVtbl {
pub base: IUnknownVtbl,
pub EnumAudioEndpoints: unsafe extern "system" fn(
this: *mut c_void,
dataFlow: u32,
dwStateMask: DWORD,
ppDevices: *mut *mut c_void,
) -> HRESULT,
pub GetDefaultAudioEndpoint: unsafe extern "system" fn(
this: *mut c_void,
dataFlow: u32,
role: u32,
ppEndpoint: *mut *mut c_void,
) -> HRESULT,
pub GetDevice: unsafe extern "system" fn(
this: *mut c_void,
pwstrId: LPCWSTR,
ppDevice: *mut *mut c_void,
) -> HRESULT,
pub RegisterEndpointNotificationCallback: unsafe extern "system" fn(
this: *mut c_void,
pClient: *mut c_void,
) -> HRESULT,
pub UnregisterEndpointNotificationCallback: unsafe extern "system" fn(
this: *mut c_void,
pClient: *mut c_void,
) -> HRESULT,
}
/// IMMDevice vtable
#[repr(C)]
pub struct IMMDeviceVtbl {
pub base: IUnknownVtbl,
pub Activate: unsafe extern "system" fn(
this: *mut c_void,
iid: *const GUID,
dwClsCtx: DWORD,
pActivationParams: *mut c_void,
ppInterface: *mut *mut c_void,
) -> HRESULT,
pub OpenPropertyStore: unsafe extern "system" fn(
this: *mut c_void,
stgmAccess: DWORD,
ppProperties: *mut *mut c_void,
) -> HRESULT,
pub GetId: unsafe extern "system" fn(
this: *mut c_void,
ppstrId: *mut LPCWSTR,
) -> HRESULT,
pub GetState: unsafe extern "system" fn(
this: *mut c_void,
pdwState: *mut DWORD,
) -> HRESULT,
}
/// IAudioClient vtable
#[repr(C)]
pub struct IAudioClientVtbl {
pub base: IUnknownVtbl,
pub Initialize: unsafe extern "system" fn(
this: *mut c_void,
ShareMode: u32,
StreamFlags: DWORD,
hnsBufferDuration: REFERENCE_TIME,
hnsPeriodicity: REFERENCE_TIME,
pFormat: *const WAVEFORMATEX,
AudioSessionGuid: *const GUID,
) -> HRESULT,
pub GetBufferSize: unsafe extern "system" fn(
this: *mut c_void,
pNumBufferFrames: *mut UINT32,
) -> HRESULT,
pub GetStreamLatency: unsafe extern "system" fn(
this: *mut c_void,
phnsLatency: *mut REFERENCE_TIME,
) -> HRESULT,
pub GetCurrentPadding: unsafe extern "system" fn(
this: *mut c_void,
pNumPaddingFrames: *mut UINT32,
) -> HRESULT,
pub IsFormatSupported: unsafe extern "system" fn(
this: *mut c_void,
ShareMode: u32,
pFormat: *const WAVEFORMATEX,
ppClosestMatch: *mut *mut WAVEFORMATEX,
) -> HRESULT,
pub GetMixFormat: unsafe extern "system" fn(
this: *mut c_void,
ppDeviceFormat: *mut *mut WAVEFORMATEX,
) -> HRESULT,
pub GetDevicePeriod: unsafe extern "system" fn(
this: *mut c_void,
phnsDefaultDevicePeriod: *mut REFERENCE_TIME,
phnsMinimumDevicePeriod: *mut REFERENCE_TIME,
) -> HRESULT,
pub Start: unsafe extern "system" fn(this: *mut c_void) -> HRESULT,
pub Stop: unsafe extern "system" fn(this: *mut c_void) -> HRESULT,
pub Reset: unsafe extern "system" fn(this: *mut c_void) -> HRESULT,
pub SetEventHandle: unsafe extern "system" fn(
this: *mut c_void,
eventHandle: HANDLE,
) -> HRESULT,
pub GetService: unsafe extern "system" fn(
this: *mut c_void,
riid: *const GUID,
ppv: *mut *mut c_void,
) -> HRESULT,
}
/// IAudioRenderClient vtable
#[repr(C)]
pub struct IAudioRenderClientVtbl {
pub base: IUnknownVtbl,
pub GetBuffer: unsafe extern "system" fn(
this: *mut c_void,
NumFramesRequested: UINT32,
ppData: *mut *mut BYTE,
) -> HRESULT,
pub ReleaseBuffer: unsafe extern "system" fn(
this: *mut c_void,
NumFramesWritten: UINT32,
dwFlags: DWORD,
) -> HRESULT,
}
// ---------------------------------------------------------------------------
// extern "system" functions
// ---------------------------------------------------------------------------
#[link(name = "ole32")]
extern "system" {
pub fn CoInitializeEx(pvReserved: LPVOID, dwCoInit: DWORD) -> HRESULT;
pub fn CoUninitialize();
pub fn CoCreateInstance(
rclsid: *const GUID,
pUnkOuter: *mut c_void,
dwClsContext: DWORD,
riid: *const GUID,
ppv: *mut *mut c_void,
) -> HRESULT;
pub fn CoTaskMemFree(pv: LPVOID);
}
// ---------------------------------------------------------------------------
// WasapiDevice
// ---------------------------------------------------------------------------
pub struct WasapiDevice {
client: *mut c_void,
render_client: *mut c_void,
buffer_size: u32,
pub sample_rate: u32,
pub channels: u16,
bits_per_sample: u16,
is_float: bool,
}
unsafe impl Send for WasapiDevice {}
impl WasapiDevice {
/// Initialize WASAPI: COM, default endpoint, IAudioClient, mix format,
/// Initialize shared mode (50 ms buffer), GetBufferSize, GetService, Start.
pub fn new() -> Result<Self, String> {
unsafe {
// 1. CoInitializeEx
let hr = CoInitializeEx(std::ptr::null_mut(), COINIT_APARTMENTTHREADED);
if hr < 0 {
return Err(format!("CoInitializeEx failed: 0x{:08X}", hr as u32));
}
// 2. CoCreateInstance -> IMMDeviceEnumerator
let mut enumerator: *mut c_void = std::ptr::null_mut();
let hr = CoCreateInstance(
&CLSID_MMDeviceEnumerator,
std::ptr::null_mut(),
CLSCTX_ALL,
&IID_IMMDeviceEnumerator,
&mut enumerator,
);
if hr < 0 || enumerator.is_null() {
CoUninitialize();
return Err(format!("CoCreateInstance(MMDeviceEnumerator) failed: 0x{:08X}", hr as u32));
}
// 3. GetDefaultAudioEndpoint -> IMMDevice
let mut device: *mut c_void = std::ptr::null_mut();
{
let vtbl = *(enumerator as *mut *const IMMDeviceEnumeratorVtbl);
let hr = ((*vtbl).GetDefaultAudioEndpoint)(enumerator, eRender, eConsole, &mut device);
((*vtbl).base.Release)(enumerator);
if hr < 0 || device.is_null() {
CoUninitialize();
return Err(format!("GetDefaultAudioEndpoint failed: 0x{:08X}", hr as u32));
}
}
// 4. IMMDevice::Activate -> IAudioClient
let mut client: *mut c_void = std::ptr::null_mut();
{
let vtbl = *(device as *mut *const IMMDeviceVtbl);
let hr = ((*vtbl).Activate)(
device,
&IID_IAudioClient,
CLSCTX_ALL,
std::ptr::null_mut(),
&mut client,
);
((*vtbl).base.Release)(device);
if hr < 0 || client.is_null() {
CoUninitialize();
return Err(format!("IMMDevice::Activate(IAudioClient) failed: 0x{:08X}", hr as u32));
}
}
// 5. GetMixFormat
let mut mix_format_ptr: *mut WAVEFORMATEX = std::ptr::null_mut();
{
let vtbl = *(client as *mut *const IAudioClientVtbl);
let hr = ((*vtbl).GetMixFormat)(client, &mut mix_format_ptr);
if hr < 0 || mix_format_ptr.is_null() {
((*vtbl).base.Release)(client);
CoUninitialize();
return Err(format!("GetMixFormat failed: 0x{:08X}", hr as u32));
}
}
let mix_format = *mix_format_ptr;
let sample_rate = mix_format.nSamplesPerSec;
let channels = mix_format.nChannels;
let bits_per_sample = mix_format.wBitsPerSample;
// Determine float vs int
let is_float = match mix_format.wFormatTag {
WAVE_FORMAT_IEEE_FLOAT => true,
WAVE_FORMAT_EXTENSIBLE => bits_per_sample == 32,
_ => false,
};
// 6. IAudioClient::Initialize (shared mode, 50 ms = 500_000 REFERENCE_TIME)
const BUFFER_DURATION: REFERENCE_TIME = 500_000; // 50 ms in 100-ns units
let hr = {
let vtbl = *(client as *mut *const IAudioClientVtbl);
((*vtbl).Initialize)(
client,
AUDCLNT_SHAREMODE_SHARED,
0,
BUFFER_DURATION,
0,
mix_format_ptr,
std::ptr::null(),
)
};
CoTaskMemFree(mix_format_ptr as LPVOID);
if hr < 0 {
let vtbl = *(client as *mut *const IAudioClientVtbl);
((*vtbl).base.Release)(client);
CoUninitialize();
return Err(format!("IAudioClient::Initialize failed: 0x{:08X}", hr as u32));
}
// 7. GetBufferSize
let mut buffer_size: UINT32 = 0;
{
let vtbl = *(client as *mut *const IAudioClientVtbl);
let hr = ((*vtbl).GetBufferSize)(client, &mut buffer_size);
if hr < 0 {
((*vtbl).base.Release)(client);
CoUninitialize();
return Err(format!("GetBufferSize failed: 0x{:08X}", hr as u32));
}
}
// 8. GetService -> IAudioRenderClient
let mut render_client: *mut c_void = std::ptr::null_mut();
{
let vtbl = *(client as *mut *const IAudioClientVtbl);
let hr = ((*vtbl).GetService)(client, &IID_IAudioRenderClient, &mut render_client);
if hr < 0 || render_client.is_null() {
((*vtbl).base.Release)(client);
CoUninitialize();
return Err(format!("GetService(IAudioRenderClient) failed: 0x{:08X}", hr as u32));
}
}
// 9. Start
{
let vtbl = *(client as *mut *const IAudioClientVtbl);
let hr = ((*vtbl).Start)(client);
if hr < 0 {
let rc_vtbl = *(render_client as *mut *const IAudioRenderClientVtbl);
((*rc_vtbl).base.Release)(render_client);
((*vtbl).base.Release)(client);
CoUninitialize();
return Err(format!("IAudioClient::Start failed: 0x{:08X}", hr as u32));
}
}
Ok(WasapiDevice {
client,
render_client,
buffer_size,
sample_rate,
channels,
bits_per_sample,
is_float,
})
}
}
/// Write f32 samples to the audio device.
/// Returns the number of frames actually written.
pub fn write_samples(&self, samples: &[f32]) -> Result<usize, String> {
unsafe {
// GetCurrentPadding
let mut padding: UINT32 = 0;
{
let vtbl = *(self.client as *mut *const IAudioClientVtbl);
let hr = ((*vtbl).GetCurrentPadding)(self.client, &mut padding);
if hr < 0 {
return Err(format!("GetCurrentPadding failed: 0x{:08X}", hr as u32));
}
}
let available_frames = if self.buffer_size > padding {
self.buffer_size - padding
} else {
return Ok(0);
};
let samples_per_frame = self.channels as usize;
let input_frames = samples.len() / samples_per_frame;
let frames_to_write = available_frames.min(input_frames as u32);
if frames_to_write == 0 {
return Ok(0);
}
// GetBuffer
let mut data_ptr: *mut BYTE = std::ptr::null_mut();
{
let vtbl = *(self.render_client as *mut *const IAudioRenderClientVtbl);
let hr = ((*vtbl).GetBuffer)(self.render_client, frames_to_write, &mut data_ptr);
if hr < 0 || data_ptr.is_null() {
return Err(format!("GetBuffer failed: 0x{:08X}", hr as u32));
}
}
// Write samples
let total_samples = frames_to_write as usize * samples_per_frame;
if self.is_float {
// Write f32 directly
let dst = std::slice::from_raw_parts_mut(data_ptr as *mut f32, total_samples);
let src_len = total_samples.min(samples.len());
dst[..src_len].copy_from_slice(&samples[..src_len]);
if src_len < total_samples {
for s in &mut dst[src_len..] {
*s = 0.0;
}
}
} else {
// Convert f32 to i16
let dst = std::slice::from_raw_parts_mut(data_ptr as *mut i16, total_samples);
for i in 0..total_samples {
let val = if i < samples.len() { samples[i] } else { 0.0 };
dst[i] = (val.clamp(-1.0, 1.0) * 32767.0) as i16;
}
}
// ReleaseBuffer
{
let vtbl = *(self.render_client as *mut *const IAudioRenderClientVtbl);
let hr = ((*vtbl).ReleaseBuffer)(self.render_client, frames_to_write, 0);
if hr < 0 {
return Err(format!("ReleaseBuffer failed: 0x{:08X}", hr as u32));
}
}
Ok(frames_to_write as usize)
}
}
/// Returns the allocated buffer size in frames.
pub fn buffer_frames(&self) -> u32 {
self.buffer_size
}
}
impl Drop for WasapiDevice {
fn drop(&mut self) {
unsafe {
// Stop the audio stream
let client_vtbl = *(self.client as *mut *const IAudioClientVtbl);
((*client_vtbl).Stop)(self.client);
// Release IAudioRenderClient
let rc_vtbl = *(self.render_client as *mut *const IAudioRenderClientVtbl);
((*rc_vtbl).base.Release)(self.render_client);
// Release IAudioClient
((*client_vtbl).base.Release)(self.client);
// Uninitialize COM
CoUninitialize();
}
}
}

View File

@@ -0,0 +1,240 @@
use crate::AudioClip;
// ---------------------------------------------------------------------------
// Helper readers
// ---------------------------------------------------------------------------
fn read_u16_le(data: &[u8], offset: usize) -> Result<u16, String> {
if offset + 2 > data.len() {
return Err(format!("read_u16_le: offset {} out of bounds (len={})", offset, data.len()));
}
Ok(u16::from_le_bytes([data[offset], data[offset + 1]]))
}
fn read_u32_le(data: &[u8], offset: usize) -> Result<u32, String> {
if offset + 4 > data.len() {
return Err(format!("read_u32_le: offset {} out of bounds (len={})", offset, data.len()));
}
Ok(u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]))
}
fn read_i16_le(data: &[u8], offset: usize) -> Result<i16, String> {
if offset + 2 > data.len() {
return Err(format!("read_i16_le: offset {} out of bounds (len={})", offset, data.len()));
}
Ok(i16::from_le_bytes([data[offset], data[offset + 1]]))
}
/// Search for a four-byte chunk ID starting after `start` and return its
/// (data_offset, data_size) pair, where data_offset is the first byte of the
/// chunk's payload.
fn find_chunk(data: &[u8], id: &[u8; 4], start: usize) -> Option<(usize, u32)> {
let mut pos = start;
while pos + 8 <= data.len() {
if &data[pos..pos + 4] == id {
let size = u32::from_le_bytes([
data[pos + 4],
data[pos + 5],
data[pos + 6],
data[pos + 7],
]);
return Some((pos + 8, size));
}
// Skip this chunk: header (8 bytes) + size, padded to even
let size = u32::from_le_bytes([
data[pos + 4],
data[pos + 5],
data[pos + 6],
data[pos + 7],
]) as usize;
let padded = size + (size & 1); // RIFF chunks are word-aligned
pos += 8 + padded;
}
None
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/// Parse a PCM 16-bit WAV file from raw bytes into an [`AudioClip`].
pub fn parse_wav(data: &[u8]) -> Result<AudioClip, String> {
// Minimum viable WAV: RIFF(4) + size(4) + WAVE(4) = 12 bytes
if data.len() < 12 {
return Err("WAV data too short".to_string());
}
// RIFF header
if &data[0..4] != b"RIFF" {
return Err("Missing RIFF header".to_string());
}
if &data[8..12] != b"WAVE" {
return Err("Missing WAVE format identifier".to_string());
}
// --- fmt chunk (search from byte 12) ---
let (fmt_offset, fmt_size) =
find_chunk(data, b"fmt ", 12).ok_or("Missing fmt chunk")?;
if fmt_size < 16 {
return Err(format!("fmt chunk too small: {}", fmt_size));
}
let format_tag = read_u16_le(data, fmt_offset)?;
if format_tag != 1 {
return Err(format!("Unsupported WAV format tag: {} (only PCM=1 is supported)", format_tag));
}
let channels = read_u16_le(data, fmt_offset + 2)?;
if channels != 1 && channels != 2 {
return Err(format!("Unsupported channel count: {}", channels));
}
let sample_rate = read_u32_le(data, fmt_offset + 4)?;
// byte_rate = fmt_offset + 8 (skip)
// block_align = fmt_offset + 12 (skip)
let bits_per_sample = read_u16_le(data, fmt_offset + 14)?;
if bits_per_sample != 16 {
return Err(format!("Unsupported bits per sample: {} (only 16-bit is supported)", bits_per_sample));
}
// --- data chunk ---
let (data_offset, data_size) =
find_chunk(data, b"data", 12).ok_or("Missing data chunk")?;
let data_end = data_offset + data_size as usize;
if data_end > data.len() {
return Err("data chunk extends beyond end of file".to_string());
}
// Each sample is 2 bytes (16-bit PCM).
let sample_count = data_size as usize / 2;
let mut samples = Vec::with_capacity(sample_count);
for i in 0..sample_count {
let raw = read_i16_le(data, data_offset + i * 2)?;
// Convert i16 [-32768, 32767] to f32 [-1.0, ~1.0]
samples.push(raw as f32 / 32768.0);
}
Ok(AudioClip::new(samples, sample_rate, channels))
}
/// Generate a minimal PCM 16-bit mono WAV file from f32 samples.
/// Used for round-trip testing.
pub fn generate_wav_bytes(samples_f32: &[f32], sample_rate: u32) -> Vec<u8> {
let channels: u16 = 1;
let bits_per_sample: u16 = 16;
let byte_rate = sample_rate * channels as u32 * bits_per_sample as u32 / 8;
let block_align: u16 = channels * bits_per_sample / 8;
let data_size = (samples_f32.len() * 2) as u32; // 2 bytes per i16 sample
let riff_size = 4 + 8 + 16 + 8 + data_size; // "WAVE" + fmt chunk + data chunk
let mut out: Vec<u8> = Vec::with_capacity(12 + 8 + 16 + 8 + data_size as usize);
// RIFF header
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&riff_size.to_le_bytes());
out.extend_from_slice(b"WAVE");
// fmt chunk
out.extend_from_slice(b"fmt ");
out.extend_from_slice(&16u32.to_le_bytes()); // chunk size
out.extend_from_slice(&1u16.to_le_bytes()); // PCM format tag
out.extend_from_slice(&channels.to_le_bytes());
out.extend_from_slice(&sample_rate.to_le_bytes());
out.extend_from_slice(&byte_rate.to_le_bytes());
out.extend_from_slice(&block_align.to_le_bytes());
out.extend_from_slice(&bits_per_sample.to_le_bytes());
// data chunk
out.extend_from_slice(b"data");
out.extend_from_slice(&data_size.to_le_bytes());
for &s in samples_f32 {
let clamped = s.clamp(-1.0, 1.0);
let raw = (clamped * 32767.0) as i16;
out.extend_from_slice(&raw.to_le_bytes());
}
out
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_valid_wav() {
// Generate a 440 Hz sine wave at 44100 Hz, 0.1 s
let sample_rate = 44100u32;
let num_samples = 4410usize;
let samples: Vec<f32> = (0..num_samples)
.map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / sample_rate as f32).sin())
.collect();
let wav_bytes = generate_wav_bytes(&samples, sample_rate);
let clip = parse_wav(&wav_bytes).expect("parse_wav failed");
assert_eq!(clip.sample_rate, sample_rate);
assert_eq!(clip.channels, 1);
assert_eq!(clip.frame_count(), num_samples);
}
#[test]
fn sample_conversion_accuracy() {
// A single-sample WAV: value = 16384 (half of i16 max positive)
// Expected f32: 16384 / 32768 = 0.5
let sample_rate = 44100u32;
let samples_f32 = vec![0.5f32];
let wav_bytes = generate_wav_bytes(&samples_f32, sample_rate);
let clip = parse_wav(&wav_bytes).expect("parse_wav failed");
assert_eq!(clip.samples.len(), 1);
// 0.5 -> i16(16383) -> f32(16383/32768) ≈ 0.49997
assert!((clip.samples[0] - 0.5f32).abs() < 0.001, "got {}", clip.samples[0]);
}
#[test]
fn invalid_riff() {
let bad_data = b"BADH\x00\x00\x00\x00WAVE";
let result = parse_wav(bad_data);
assert!(result.is_err());
assert!(result.unwrap_err().contains("RIFF"));
}
#[test]
fn too_short() {
let short_data = b"RIF";
let result = parse_wav(short_data);
assert!(result.is_err());
assert!(result.unwrap_err().contains("too short"));
}
#[test]
fn roundtrip() {
let original: Vec<f32> = vec![0.0, 0.25, 0.5, -0.25, -0.5, 1.0, -1.0];
let wav_bytes = generate_wav_bytes(&original, 44100);
let clip = parse_wav(&wav_bytes).expect("roundtrip parse failed");
assert_eq!(clip.samples.len(), original.len());
for (orig, decoded) in original.iter().zip(clip.samples.iter()) {
// i16 quantization error < 0.001
assert!(
(orig - decoded).abs() < 0.001,
"orig={} decoded={}",
orig,
decoded
);
}
}
}

View File

@@ -0,0 +1,8 @@
[package]
name = "voltex_editor"
version = "0.1.0"
edition = "2021"
[dependencies]
bytemuck = { workspace = true }
wgpu = { workspace = true }

View File

@@ -0,0 +1,123 @@
use bytemuck::{Pod, Zeroable};
use crate::font::FontAtlas;
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct DrawVertex {
pub position: [f32; 2],
pub uv: [f32; 2],
pub color: [u8; 4],
}
pub struct DrawCommand {
pub index_offset: u32,
pub index_count: u32,
}
pub struct DrawList {
pub vertices: Vec<DrawVertex>,
pub indices: Vec<u16>,
pub commands: Vec<DrawCommand>,
}
impl DrawList {
pub fn new() -> Self {
DrawList {
vertices: Vec::new(),
indices: Vec::new(),
commands: Vec::new(),
}
}
pub fn clear(&mut self) {
self.vertices.clear();
self.indices.clear();
self.commands.clear();
}
/// Add a solid-color rectangle. UV is (0,0) for solid color rendering.
pub fn add_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: [u8; 4]) {
self.add_rect_uv(x, y, w, h, 0.0, 0.0, 0.0, 0.0, color);
}
/// Add a textured quad with explicit UV coordinates.
pub fn add_rect_uv(
&mut self,
x: f32, y: f32, w: f32, h: f32,
u0: f32, v0: f32, u1: f32, v1: f32,
color: [u8; 4],
) {
let index_offset = self.indices.len() as u32;
let base_vertex = self.vertices.len() as u16;
// 4 vertices: top-left, top-right, bottom-right, bottom-left
self.vertices.push(DrawVertex { position: [x, y ], uv: [u0, v0], color });
self.vertices.push(DrawVertex { position: [x + w, y ], uv: [u1, v0], color });
self.vertices.push(DrawVertex { position: [x + w, y + h], uv: [u1, v1], color });
self.vertices.push(DrawVertex { position: [x, y + h], uv: [u0, v1], color });
// 2 triangles = 6 indices
self.indices.push(base_vertex);
self.indices.push(base_vertex + 1);
self.indices.push(base_vertex + 2);
self.indices.push(base_vertex);
self.indices.push(base_vertex + 2);
self.indices.push(base_vertex + 3);
self.commands.push(DrawCommand {
index_offset,
index_count: 6,
});
}
/// Add text at the given position. One quad per character, using glyph UVs from the atlas.
pub fn add_text(&mut self, font: &FontAtlas, text: &str, x: f32, y: f32, color: [u8; 4]) {
let gw = font.glyph_width as f32;
let gh = font.glyph_height as f32;
let mut cx = x;
for ch in text.chars() {
let (u0, v0, u1, v1) = font.glyph_uv(ch);
self.add_rect_uv(cx, y, gw, gh, u0, v0, u1, v1, color);
cx += gw;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::font::FontAtlas;
#[test]
fn test_add_rect_vertex_index_count() {
let mut dl = DrawList::new();
dl.add_rect(0.0, 0.0, 100.0, 50.0, [255, 0, 0, 255]);
assert_eq!(dl.vertices.len(), 4);
assert_eq!(dl.indices.len(), 6);
assert_eq!(dl.commands.len(), 1);
assert_eq!(dl.commands[0].index_count, 6);
assert_eq!(dl.commands[0].index_offset, 0);
}
#[test]
fn test_add_text_char_count() {
let font = FontAtlas::generate();
let mut dl = DrawList::new();
let text = "Hello";
dl.add_text(&font, text, 0.0, 0.0, [255, 255, 255, 255]);
// 5 chars => 5 quads => 5*4=20 vertices, 5*6=30 indices
assert_eq!(dl.vertices.len(), 5 * 4);
assert_eq!(dl.indices.len(), 5 * 6);
assert_eq!(dl.commands.len(), 5);
}
#[test]
fn test_clear() {
let mut dl = DrawList::new();
dl.add_rect(0.0, 0.0, 50.0, 50.0, [0, 0, 0, 255]);
dl.clear();
assert!(dl.vertices.is_empty());
assert!(dl.indices.is_empty());
assert!(dl.commands.is_empty());
}
}

View File

@@ -0,0 +1,313 @@
/// Bitmap font atlas for ASCII 32-126.
/// 8x12 pixel glyphs arranged in 16 columns x 6 rows = 128x72 texture.
pub struct FontAtlas {
pub width: u32,
pub height: u32,
pub glyph_width: u32,
pub glyph_height: u32,
pub pixels: Vec<u8>, // R8 grayscale
}
/// Classic 8x12 PC BIOS bitmap font data for ASCII 32-126 (95 characters).
/// Each character is 12 bytes (one byte per row, MSB = leftmost pixel).
#[rustfmt::skip]
const FONT_DATA: [u8; 95 * 12] = [
// 32: Space
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 33: !
0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x00, 0x00, 0x00,
// 34: "
0x00, 0x6C, 0x6C, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 35: #
0x00, 0x6C, 0x6C, 0xFE, 0x6C, 0xFE, 0x6C, 0x6C, 0x00, 0x00, 0x00, 0x00,
// 36: $
0x00, 0x18, 0x3E, 0x60, 0x3C, 0x06, 0x7C, 0x18, 0x00, 0x00, 0x00, 0x00,
// 37: %
0x00, 0x00, 0x62, 0x64, 0x08, 0x10, 0x26, 0x46, 0x00, 0x00, 0x00, 0x00,
// 38: &
0x00, 0x30, 0x48, 0x48, 0x30, 0x4A, 0x44, 0x3A, 0x00, 0x00, 0x00, 0x00,
// 39: '
0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 40: (
0x00, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00,
// 41: )
0x00, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00,
// 42: *
0x00, 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00,
// 43: +
0x00, 0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
// 44: ,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00,
// 45: -
0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 46: .
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
// 47: /
0x00, 0x02, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00,
// 48: 0
0x00, 0x3C, 0x66, 0x6E, 0x7E, 0x76, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 49: 1
0x00, 0x18, 0x38, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 50: 2
0x00, 0x3C, 0x66, 0x06, 0x0C, 0x18, 0x30, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 51: 3
0x00, 0x3C, 0x66, 0x06, 0x1C, 0x06, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 52: 4
0x00, 0x0C, 0x1C, 0x2C, 0x4C, 0x7E, 0x0C, 0x0C, 0x00, 0x00, 0x00, 0x00,
// 53: 5
0x00, 0x7E, 0x60, 0x7C, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 54: 6
0x00, 0x1C, 0x30, 0x60, 0x7C, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 55: 7
0x00, 0x7E, 0x06, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00,
// 56: 8
0x00, 0x3C, 0x66, 0x66, 0x3C, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 57: 9
0x00, 0x3C, 0x66, 0x66, 0x3E, 0x06, 0x0C, 0x38, 0x00, 0x00, 0x00, 0x00,
// 58: :
0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
// 59: ;
0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00,
// 60: <
0x00, 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00, 0x00, 0x00, 0x00,
// 61: =
0x00, 0x00, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 62: >
0x00, 0x60, 0x30, 0x18, 0x0C, 0x18, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00,
// 63: ?
0x00, 0x3C, 0x66, 0x06, 0x0C, 0x18, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00,
// 64: @
0x00, 0x3C, 0x66, 0x6E, 0x6A, 0x6E, 0x60, 0x3E, 0x00, 0x00, 0x00, 0x00,
// 65: A
0x00, 0x18, 0x3C, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 66: B
0x00, 0x7C, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x7C, 0x00, 0x00, 0x00, 0x00,
// 67: C
0x00, 0x3C, 0x66, 0x60, 0x60, 0x60, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 68: D
0x00, 0x78, 0x6C, 0x66, 0x66, 0x66, 0x6C, 0x78, 0x00, 0x00, 0x00, 0x00,
// 69: E
0x00, 0x7E, 0x60, 0x60, 0x7C, 0x60, 0x60, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 70: F
0x00, 0x7E, 0x60, 0x60, 0x7C, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00,
// 71: G
0x00, 0x3C, 0x66, 0x60, 0x6E, 0x66, 0x66, 0x3E, 0x00, 0x00, 0x00, 0x00,
// 72: H
0x00, 0x66, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 73: I
0x00, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 74: J
0x00, 0x06, 0x06, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 75: K
0x00, 0x66, 0x6C, 0x78, 0x70, 0x78, 0x6C, 0x66, 0x00, 0x00, 0x00, 0x00,
// 76: L
0x00, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 77: M
0x00, 0xC6, 0xEE, 0xFE, 0xD6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00,
// 78: N
0x00, 0x66, 0x76, 0x7E, 0x7E, 0x6E, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 79: O
0x00, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 80: P
0x00, 0x7C, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00,
// 81: Q
0x00, 0x3C, 0x66, 0x66, 0x66, 0x6A, 0x6C, 0x36, 0x00, 0x00, 0x00, 0x00,
// 82: R
0x00, 0x7C, 0x66, 0x66, 0x7C, 0x6C, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 83: S
0x00, 0x3C, 0x66, 0x60, 0x3C, 0x06, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 84: T
0x00, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
// 85: U
0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 86: V
0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00,
// 87: W
0x00, 0xC6, 0xC6, 0xC6, 0xD6, 0xFE, 0xEE, 0xC6, 0x00, 0x00, 0x00, 0x00,
// 88: X
0x00, 0x66, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 89: Y
0x00, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
// 90: Z
0x00, 0x7E, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 91: [
0x00, 0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 92: backslash
0x00, 0x40, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00,
// 93: ]
0x00, 0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 94: ^
0x00, 0x18, 0x3C, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 95: _
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00,
// 96: `
0x00, 0x30, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 97: a
0x00, 0x00, 0x00, 0x3C, 0x06, 0x3E, 0x66, 0x3E, 0x00, 0x00, 0x00, 0x00,
// 98: b
0x00, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x7C, 0x00, 0x00, 0x00, 0x00,
// 99: c
0x00, 0x00, 0x00, 0x3C, 0x66, 0x60, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 100: d
0x00, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00, 0x00, 0x00,
// 101: e
0x00, 0x00, 0x00, 0x3C, 0x66, 0x7E, 0x60, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 102: f
0x00, 0x1C, 0x30, 0x30, 0x7C, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00,
// 103: g
0x00, 0x00, 0x00, 0x3E, 0x66, 0x66, 0x3E, 0x06, 0x3C, 0x00, 0x00, 0x00,
// 104: h
0x00, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 105: i
0x00, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 106: j
0x00, 0x0C, 0x00, 0x1C, 0x0C, 0x0C, 0x0C, 0x0C, 0x78, 0x00, 0x00, 0x00,
// 107: k
0x00, 0x60, 0x60, 0x66, 0x6C, 0x78, 0x6C, 0x66, 0x00, 0x00, 0x00, 0x00,
// 108: l
0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 109: m
0x00, 0x00, 0x00, 0xEC, 0xFE, 0xD6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00,
// 110: n
0x00, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
// 111: o
0x00, 0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
// 112: p
0x00, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x00, 0x00, 0x00,
// 113: q
0x00, 0x00, 0x00, 0x3E, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x00, 0x00, 0x00,
// 114: r
0x00, 0x00, 0x00, 0x7C, 0x66, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00,
// 115: s
0x00, 0x00, 0x00, 0x3E, 0x60, 0x3C, 0x06, 0x7C, 0x00, 0x00, 0x00, 0x00,
// 116: t
0x00, 0x30, 0x30, 0x7C, 0x30, 0x30, 0x30, 0x1C, 0x00, 0x00, 0x00, 0x00,
// 117: u
0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00, 0x00, 0x00,
// 118: v
0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00,
// 119: w
0x00, 0x00, 0x00, 0xC6, 0xC6, 0xD6, 0xFE, 0x6C, 0x00, 0x00, 0x00, 0x00,
// 120: x
0x00, 0x00, 0x00, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x00, 0x00, 0x00, 0x00,
// 121: y
0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x3C, 0x00, 0x00, 0x00,
// 122: z
0x00, 0x00, 0x00, 0x7E, 0x0C, 0x18, 0x30, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 123: {
0x00, 0x0E, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0E, 0x00, 0x00, 0x00, 0x00,
// 124: |
0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
// 125: }
0x00, 0x70, 0x18, 0x18, 0x0E, 0x18, 0x18, 0x70, 0x00, 0x00, 0x00, 0x00,
// 126: ~
0x00, 0x32, 0x4C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
impl FontAtlas {
/// Generate a bitmap font atlas with real 8x12 PC BIOS-style glyphs.
/// Each glyph is 8x12 pixels. Atlas is 16 cols x 6 rows = 128x72.
pub fn generate() -> Self {
let glyph_width: u32 = 8;
let glyph_height: u32 = 12;
let cols: u32 = 16;
let rows: u32 = 6;
let width = cols * glyph_width; // 128
let height = rows * glyph_height; // 72
let mut pixels = vec![0u8; (width * height) as usize];
// Unpack 1-bit-per-pixel FONT_DATA into R8 pixel atlas
for code in 32u8..=126u8 {
let index = (code - 32) as u32;
let col = index % cols;
let row = index / cols;
let base_x = col * glyph_width;
let base_y = row * glyph_height;
let data_offset = (index as usize) * 12;
for py in 0..12u32 {
let row_byte = FONT_DATA[data_offset + py as usize];
for px in 0..8u32 {
if (row_byte >> (7 - px)) & 1 != 0 {
let pixel_idx = ((base_y + py) * width + (base_x + px)) as usize;
pixels[pixel_idx] = 255;
}
}
}
}
// Ensure pixel (0,0) is white (255) so solid-color rects can sample
// UV (0,0) and get full brightness for tinting.
pixels[0] = 255;
FontAtlas {
width,
height,
glyph_width,
glyph_height,
pixels,
}
}
/// Returns (u0, v0, u1, v1) UV coordinates for a given character.
/// Returns coordinates for space if character is out of ASCII range.
pub fn glyph_uv(&self, ch: char) -> (f32, f32, f32, f32) {
let code = ch as u32;
let index = if code >= 32 && code <= 126 {
code - 32
} else {
0 // space
};
let cols = self.width / self.glyph_width;
let col = index % cols;
let row = index / cols;
let u0 = (col * self.glyph_width) as f32 / self.width as f32;
let v0 = (row * self.glyph_height) as f32 / self.height as f32;
let u1 = u0 + self.glyph_width as f32 / self.width as f32;
let v1 = v0 + self.glyph_height as f32 / self.height as f32;
(u0, v0, u1, v1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_size() {
let atlas = FontAtlas::generate();
assert_eq!(atlas.width, 128);
assert_eq!(atlas.height, 72);
assert_eq!(atlas.pixels.len(), (128 * 72) as usize);
}
#[test]
fn test_glyph_uv_space() {
let atlas = FontAtlas::generate();
let (u0, v0, u1, v1) = atlas.glyph_uv(' ');
assert!((u0 - 0.0).abs() < 1e-6);
assert!((v0 - 0.0).abs() < 1e-6);
assert!((u1 - 8.0 / 128.0).abs() < 1e-6);
assert!((v1 - 12.0 / 72.0).abs() < 1e-6);
}
#[test]
fn test_glyph_uv_a() {
let atlas = FontAtlas::generate();
// 'A' = ASCII 65, index = 65 - 32 = 33
// col = 33 % 16 = 1, row = 33 / 16 = 2
let (u0, v0, u1, v1) = atlas.glyph_uv('A');
let expected_u0 = 1.0 * 8.0 / 128.0;
let expected_v0 = 2.0 * 12.0 / 72.0;
let expected_u1 = expected_u0 + 8.0 / 128.0;
let expected_v1 = expected_v0 + 12.0 / 72.0;
assert!((u0 - expected_u0).abs() < 1e-6, "u0 mismatch: {} vs {}", u0, expected_u0);
assert!((v0 - expected_v0).abs() < 1e-6, "v0 mismatch: {} vs {}", v0, expected_v0);
assert!((u1 - expected_u1).abs() < 1e-6);
assert!((v1 - expected_v1).abs() < 1e-6);
}
}

View File

@@ -0,0 +1,32 @@
/// Simple cursor-based layout state for immediate mode UI.
pub struct LayoutState {
pub cursor_x: f32,
pub cursor_y: f32,
pub indent: f32,
pub line_height: f32,
pub padding: f32,
}
impl LayoutState {
/// Create a new layout state starting at (x, y).
pub fn new(x: f32, y: f32) -> Self {
LayoutState {
cursor_x: x,
cursor_y: y,
indent: x,
line_height: 12.0,
padding: 4.0,
}
}
/// Advance to the next line: cursor_y += line_height + padding, cursor_x = indent.
pub fn advance_line(&mut self) {
self.cursor_y += self.line_height + self.padding;
self.cursor_x = self.indent;
}
/// Advance horizontally: cursor_x += width + padding.
pub fn advance_x(&mut self, width: f32) {
self.cursor_x += width + self.padding;
}
}

View File

@@ -0,0 +1,12 @@
pub mod font;
pub mod draw_list;
pub mod layout;
pub mod renderer;
pub mod ui_context;
pub mod widgets;
pub use font::FontAtlas;
pub use draw_list::{DrawVertex, DrawCommand, DrawList};
pub use layout::LayoutState;
pub use renderer::UiRenderer;
pub use ui_context::UiContext;

View File

@@ -0,0 +1,339 @@
use wgpu::util::DeviceExt;
use crate::draw_list::{DrawList, DrawVertex};
use crate::font::FontAtlas;
/// Vertex buffer layout for DrawVertex: position(Float32x2), uv(Float32x2), color(Unorm8x4).
const VERTEX_LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<DrawVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
// position: vec2<f32>
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x2,
},
// uv: vec2<f32>
wgpu::VertexAttribute {
offset: 8,
shader_location: 1,
format: wgpu::VertexFormat::Float32x2,
},
// color: vec4<f32> (from [u8;4] via Unorm8x4)
wgpu::VertexAttribute {
offset: 16,
shader_location: 2,
format: wgpu::VertexFormat::Unorm8x4,
},
],
};
/// GPU-side UI renderer that turns a DrawList into a rendered frame.
pub struct UiRenderer {
pipeline: wgpu::RenderPipeline,
#[allow(dead_code)]
bind_group_layout: wgpu::BindGroupLayout,
font_bind_group: wgpu::BindGroup,
uniform_buffer: wgpu::Buffer,
projection: [f32; 16],
last_screen_w: f32,
last_screen_h: f32,
}
impl UiRenderer {
/// Create a new UI renderer.
///
/// `font` is used to build the font atlas GPU texture. Pixel (0,0) must be 255
/// so solid-color rects can sample UV (0,0) for white.
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
surface_format: wgpu::TextureFormat,
font: &FontAtlas,
) -> Self {
// --- Shader ---
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("UI Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("ui_shader.wgsl").into()),
});
// --- Bind group layout: uniform + texture + sampler ---
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("UI Bind Group Layout"),
entries: &[
// @binding(0): uniform buffer (projection mat4x4)
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
// @binding(1): texture
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
// @binding(2): sampler
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
// --- Pipeline layout ---
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("UI Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
immediate_size: 0,
});
// --- Render pipeline: alpha blend, no depth ---
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("UI Render Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[VERTEX_LAYOUT],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
// --- Font atlas GPU texture (R8Unorm) ---
let atlas_texture = device.create_texture_with_data(
queue,
&wgpu::TextureDescriptor {
label: Some("UI Font Atlas"),
size: wgpu::Extent3d {
width: font.width,
height: font.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
wgpu::util::TextureDataOrder::LayerMajor,
&font.pixels,
);
let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("UI Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
// --- Uniform buffer (projection) ---
let projection = ortho_projection(800.0, 600.0);
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("UI Uniform Buffer"),
contents: bytemuck::cast_slice(&projection),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
// --- Bind group ---
let font_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("UI Font Bind Group"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&atlas_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
UiRenderer {
pipeline,
bind_group_layout: bind_group_layout,
font_bind_group,
uniform_buffer,
projection,
last_screen_w: 800.0,
last_screen_h: 600.0,
}
}
/// Render the draw list onto the given target view.
///
/// Uses `LoadOp::Load` so the UI overlays whatever was previously rendered.
/// For a standalone UI demo, clear the surface before calling this.
pub fn render(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
target_view: &wgpu::TextureView,
draw_list: &DrawList,
screen_w: f32,
screen_h: f32,
) {
if draw_list.vertices.is_empty() || draw_list.indices.is_empty() {
return;
}
// Update projection if screen size changed
if (screen_w - self.last_screen_w).abs() > 0.5
|| (screen_h - self.last_screen_h).abs() > 0.5
{
self.projection = ortho_projection(screen_w, screen_h);
queue.write_buffer(
&self.uniform_buffer,
0,
bytemuck::cast_slice(&self.projection),
);
self.last_screen_w = screen_w;
self.last_screen_h = screen_h;
}
// Create vertex and index buffers from DrawList
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("UI Vertex Buffer"),
contents: bytemuck::cast_slice(&draw_list.vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("UI Index Buffer"),
contents: bytemuck::cast_slice(&draw_list.indices),
usage: wgpu::BufferUsages::INDEX,
});
// Begin render pass (Load to overlay on existing content)
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("UI Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
multiview_mask: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.font_bind_group, &[]);
pass.set_vertex_buffer(0, vertex_buffer.slice(..));
pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
// Draw each command
for cmd in &draw_list.commands {
pass.draw_indexed(
cmd.index_offset..cmd.index_offset + cmd.index_count,
0,
0..1,
);
}
}
}
}
/// Orthographic projection: left=0, right=w, top=0, bottom=h (Y down), near=-1, far=1.
/// Returns a column-major 4x4 matrix as [f32; 16].
fn ortho_projection(w: f32, h: f32) -> [f32; 16] {
let l = 0.0_f32;
let r = w;
let t = 0.0_f32;
let b = h;
let n = -1.0_f32;
let f = 1.0_f32;
// Column-major layout for wgpu/WGSL mat4x4
[
2.0 / (r - l), 0.0, 0.0, 0.0,
0.0, 2.0 / (t - b), 0.0, 0.0,
0.0, 0.0, 1.0 / (f - n), 0.0,
-(r + l) / (r - l), -(t + b) / (t - b), -n / (f - n), 1.0,
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ortho_projection_corners() {
let proj = ortho_projection(800.0, 600.0);
// Top-left (0,0) should map to NDC (-1, 1)
let x = proj[0] * 0.0 + proj[4] * 0.0 + proj[8] * 0.0 + proj[12];
let y = proj[1] * 0.0 + proj[5] * 0.0 + proj[9] * 0.0 + proj[13];
assert!((x - (-1.0)).abs() < 1e-5, "top-left x: {}", x);
assert!((y - 1.0).abs() < 1e-5, "top-left y: {}", y);
// Bottom-right (800, 600) should map to NDC (1, -1)
let x2 = proj[0] * 800.0 + proj[4] * 600.0 + proj[8] * 0.0 + proj[12];
let y2 = proj[1] * 800.0 + proj[5] * 600.0 + proj[9] * 0.0 + proj[13];
assert!((x2 - 1.0).abs() < 1e-5, "bot-right x: {}", x2);
assert!((y2 - (-1.0)).abs() < 1e-5, "bot-right y: {}", y2);
}
}

View File

@@ -0,0 +1,81 @@
use crate::draw_list::DrawList;
use crate::font::FontAtlas;
use crate::layout::LayoutState;
pub struct UiContext {
pub hot: Option<u64>,
pub active: Option<u64>,
pub draw_list: DrawList,
pub layout: LayoutState,
pub mouse_x: f32,
pub mouse_y: f32,
pub mouse_down: bool,
pub mouse_clicked: bool,
pub mouse_released: bool,
pub screen_width: f32,
pub screen_height: f32,
pub font: FontAtlas,
id_counter: u64,
prev_mouse_down: bool,
}
impl UiContext {
/// Create a new UiContext for the given screen dimensions.
pub fn new(screen_w: f32, screen_h: f32) -> Self {
UiContext {
hot: None,
active: None,
draw_list: DrawList::new(),
layout: LayoutState::new(0.0, 0.0),
mouse_x: 0.0,
mouse_y: 0.0,
mouse_down: false,
mouse_clicked: false,
mouse_released: false,
screen_width: screen_w,
screen_height: screen_h,
font: FontAtlas::generate(),
id_counter: 0,
prev_mouse_down: false,
}
}
/// Begin a new frame: clear draw list, reset id counter, update mouse state.
pub fn begin_frame(&mut self, mx: f32, my: f32, mouse_down: bool) {
self.draw_list.clear();
self.id_counter = 0;
self.hot = None;
self.mouse_x = mx;
self.mouse_y = my;
// Compute transitions
self.mouse_clicked = !self.prev_mouse_down && mouse_down;
self.mouse_released = self.prev_mouse_down && !mouse_down;
self.mouse_down = mouse_down;
self.prev_mouse_down = mouse_down;
// Reset layout to top-left
self.layout = LayoutState::new(0.0, 0.0);
}
/// End the current frame.
pub fn end_frame(&mut self) {
// Nothing for now — GPU submission will hook in here later.
}
/// Generate a new unique ID for this frame.
pub fn gen_id(&mut self) -> u64 {
self.id_counter += 1;
self.id_counter
}
/// Check if the mouse cursor is inside the given rectangle.
pub fn mouse_in_rect(&self, x: f32, y: f32, w: f32, h: f32) -> bool {
self.mouse_x >= x
&& self.mouse_x < x + w
&& self.mouse_y >= y
&& self.mouse_y < y + h
}
}

View File

@@ -0,0 +1,34 @@
struct UiUniform {
projection: mat4x4<f32>,
};
@group(0) @binding(0) var<uniform> ui_uniform: UiUniform;
@group(0) @binding(1) var t_atlas: texture_2d<f32>;
@group(0) @binding(2) var s_atlas: sampler;
struct VertexInput {
@location(0) position: vec2<f32>,
@location(1) uv: vec2<f32>,
@location(2) color: vec4<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) color: vec4<f32>,
};
@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.clip_position = ui_uniform.projection * vec4<f32>(in.position, 0.0, 1.0);
out.uv = in.uv;
out.color = in.color;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let tex_alpha = textureSample(t_atlas, s_atlas, in.uv).r;
return vec4<f32>(in.color.rgb * tex_alpha, in.color.a * tex_alpha);
}

View File

@@ -0,0 +1,281 @@
use crate::ui_context::UiContext;
// Color palette
const COLOR_BG: [u8; 4] = [0x2B, 0x2B, 0x2B, 0xFF];
const COLOR_BUTTON: [u8; 4] = [0x44, 0x44, 0x55, 0xFF];
const COLOR_BUTTON_HOT: [u8; 4] = [0x55, 0x66, 0x88, 0xFF];
const COLOR_BUTTON_ACTIVE: [u8; 4] = [0x44, 0x88, 0xFF, 0xFF];
const COLOR_TEXT: [u8; 4] = [0xEE, 0xEE, 0xEE, 0xFF];
const COLOR_PANEL: [u8; 4] = [0x33, 0x33, 0x33, 0xFF];
const COLOR_SLIDER_BG: [u8; 4] = [0x44, 0x44, 0x44, 0xFF];
const COLOR_SLIDER_HANDLE: [u8; 4] = [0x88, 0x88, 0xFF, 0xFF];
const COLOR_CHECK_BG: [u8; 4] = [0x44, 0x44, 0x44, 0xFF];
const COLOR_CHECK_MARK: [u8; 4] = [0x88, 0xFF, 0x88, 0xFF];
impl UiContext {
/// Draw text at the current cursor position and advance to the next line.
pub fn text(&mut self, text: &str) {
let x = self.layout.cursor_x;
let y = self.layout.cursor_y;
// We need to clone font info for the borrow checker
let gw = self.font.glyph_width as f32;
let gh = self.font.glyph_height as f32;
// Draw each character
let mut cx = x;
for ch in text.chars() {
let (u0, v0, u1, v1) = self.font.glyph_uv(ch);
self.draw_list.add_rect_uv(cx, y, gw, gh, u0, v0, u1, v1, COLOR_TEXT);
cx += gw;
}
self.layout.advance_line();
}
/// Draw a button with the given label. Returns true if clicked this frame.
pub fn button(&mut self, label: &str) -> bool {
let id = self.gen_id();
let gw = self.font.glyph_width as f32;
let gh = self.font.glyph_height as f32;
let padding = self.layout.padding;
let text_w = label.len() as f32 * gw;
let btn_w = text_w + padding * 2.0;
let btn_h = gh + padding * 2.0;
let x = self.layout.cursor_x;
let y = self.layout.cursor_y;
let hovered = self.mouse_in_rect(x, y, btn_w, btn_h);
if hovered {
self.hot = Some(id);
if self.mouse_down {
self.active = Some(id);
}
}
// Determine color
let bg_color = if self.active == Some(id) && hovered {
COLOR_BUTTON_ACTIVE
} else if self.hot == Some(id) {
COLOR_BUTTON_HOT
} else {
COLOR_BUTTON
};
// Draw background rect
self.draw_list.add_rect(x, y, btn_w, btn_h, bg_color);
// Draw text centered inside button
let text_x = x + padding;
let text_y = y + padding;
let mut cx = text_x;
for ch in label.chars() {
let (u0, v0, u1, v1) = self.font.glyph_uv(ch);
self.draw_list.add_rect_uv(cx, text_y, gw, gh, u0, v0, u1, v1, COLOR_TEXT);
cx += gw;
}
self.layout.advance_line();
// Return true if mouse was released over this button while it was active
let clicked = hovered && self.mouse_released && self.active == Some(id);
if self.mouse_released {
if self.active == Some(id) {
self.active = None;
}
}
clicked
}
/// Draw a horizontal slider. Returns the (possibly new) value after interaction.
pub fn slider(&mut self, label: &str, value: f32, min: f32, max: f32) -> f32 {
let id = self.gen_id();
let gw = self.font.glyph_width as f32;
let gh = self.font.glyph_height as f32;
let padding = self.layout.padding;
let slider_w = 150.0_f32;
let slider_h = gh + padding * 2.0;
let handle_w = 10.0_f32;
let x = self.layout.cursor_x;
let y = self.layout.cursor_y;
let hovered = self.mouse_in_rect(x, y, slider_w, slider_h);
if hovered && self.mouse_clicked {
self.active = Some(id);
}
let mut new_value = value;
if self.active == Some(id) {
if self.mouse_down {
// Map mouse_x to value
let t = ((self.mouse_x - x - handle_w / 2.0) / (slider_w - handle_w)).clamp(0.0, 1.0);
new_value = min + t * (max - min);
} else if self.mouse_released {
self.active = None;
}
}
// Clamp value
new_value = new_value.clamp(min, max);
// Draw background bar
self.draw_list.add_rect(x, y, slider_w, slider_h, COLOR_SLIDER_BG);
// Draw handle
let t = if (max - min).abs() < 1e-6 {
0.0
} else {
(new_value - min) / (max - min)
};
let handle_x = x + t * (slider_w - handle_w);
self.draw_list.add_rect(handle_x, y, handle_w, slider_h, COLOR_SLIDER_HANDLE);
// Draw label to the right of the slider
let label_x = x + slider_w + padding;
let label_y = y + padding;
let mut cx = label_x;
for ch in label.chars() {
let (u0, v0, u1, v1) = self.font.glyph_uv(ch);
self.draw_list.add_rect_uv(cx, label_y, gw, gh, u0, v0, u1, v1, COLOR_TEXT);
cx += gw;
}
self.layout.advance_line();
new_value
}
/// Draw a checkbox. Returns the new checked state (toggled on click).
pub fn checkbox(&mut self, label: &str, checked: bool) -> bool {
let id = self.gen_id();
let gw = self.font.glyph_width as f32;
let gh = self.font.glyph_height as f32;
let padding = self.layout.padding;
let box_size = gh;
let x = self.layout.cursor_x;
let y = self.layout.cursor_y;
let hovered = self.mouse_in_rect(x, y, box_size + padding + label.len() as f32 * gw, gh + padding);
if hovered {
self.hot = Some(id);
}
let mut new_checked = checked;
if hovered && self.mouse_clicked {
new_checked = !checked;
}
// Draw checkbox background
self.draw_list.add_rect(x, y, box_size, box_size, COLOR_CHECK_BG);
// Draw check mark if checked
if new_checked {
let inner = 3.0;
self.draw_list.add_rect(
x + inner,
y + inner,
box_size - inner * 2.0,
box_size - inner * 2.0,
COLOR_CHECK_MARK,
);
}
// Draw label
let label_x = x + box_size + padding;
let label_y = y;
let mut cx = label_x;
for ch in label.chars() {
let (u0, v0, u1, v1) = self.font.glyph_uv(ch);
self.draw_list.add_rect_uv(cx, label_y, gw, gh, u0, v0, u1, v1, COLOR_TEXT);
cx += gw;
}
self.layout.advance_line();
new_checked
}
/// Begin a panel: draw background and title, set cursor inside panel.
pub fn begin_panel(&mut self, title: &str, x: f32, y: f32, w: f32, h: f32) {
let gh = self.font.glyph_height as f32;
let padding = self.layout.padding;
// Draw panel background
self.draw_list.add_rect(x, y, w, h, COLOR_PANEL);
// Draw title bar (slightly darker background handled by same panel color here)
let title_bar_h = gh + padding * 2.0;
self.draw_list.add_rect(x, y, w, title_bar_h, COLOR_BG);
// Draw title text
let gw = self.font.glyph_width as f32;
let mut cx = x + padding;
let ty = y + padding;
for ch in title.chars() {
let (u0, v0, u1, v1) = self.font.glyph_uv(ch);
self.draw_list.add_rect_uv(cx, ty, gw, gh, u0, v0, u1, v1, COLOR_TEXT);
cx += gw;
}
// Set cursor to inside the panel (below title bar)
self.layout = crate::layout::LayoutState::new(x + padding, y + title_bar_h + padding);
}
/// End a panel — currently a no-op; cursor remains where it was.
pub fn end_panel(&mut self) {
// Nothing for now; future could restore outer cursor state.
}
}
#[cfg(test)]
mod tests {
use crate::ui_context::UiContext;
#[test]
fn test_button_returns_false_when_not_clicked() {
let mut ctx = UiContext::new(800.0, 600.0);
// Mouse is at (500, 500) — far from any button
ctx.begin_frame(500.0, 500.0, false);
let result = ctx.button("Click Me");
assert!(!result, "button should return false when mouse is not over it");
}
#[test]
fn test_button_returns_true_when_clicked() {
let mut ctx = UiContext::new(800.0, 600.0);
// Frame 1: mouse over button, pressed down
// Button will be at layout cursor (0, 0) with some width/height
// glyph_width=8, "OK"=2 chars, btn_w = 2*8 + 4*2 = 24, btn_h = 12 + 4*2 = 20
ctx.begin_frame(10.0, 5.0, true);
let _ = ctx.button("OK");
// Frame 2: mouse still over button, released
ctx.begin_frame(10.0, 5.0, false);
let result = ctx.button("OK");
assert!(result, "button should return true when clicked and released");
}
#[test]
fn test_slider_returns_clamped_value() {
let mut ctx = UiContext::new(800.0, 600.0);
ctx.begin_frame(0.0, 0.0, false);
// Value above max should be clamped
let v = ctx.slider("test", 150.0, 0.0, 100.0);
assert!((v - 100.0).abs() < 1e-6, "slider should clamp to max: got {}", v);
ctx.begin_frame(0.0, 0.0, false);
// Value below min should be clamped
let v2 = ctx.slider("test", -10.0, 0.0, 100.0);
assert!((v2 - 0.0).abs() < 1e-6, "slider should clamp to min: got {}", v2);
}
}

View File

@@ -0,0 +1,122 @@
// crates/voltex_math/src/aabb.rs
use crate::Vec3;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AABB {
pub min: Vec3,
pub max: Vec3,
}
impl AABB {
pub fn new(min: Vec3, max: Vec3) -> Self {
Self { min, max }
}
pub fn from_center_half_extents(center: Vec3, half: Vec3) -> Self {
Self {
min: center - half,
max: center + half,
}
}
pub fn center(&self) -> Vec3 {
(self.min + self.max) * 0.5
}
pub fn half_extents(&self) -> Vec3 {
(self.max - self.min) * 0.5
}
pub fn contains_point(&self, p: Vec3) -> bool {
p.x >= self.min.x && p.x <= self.max.x
&& p.y >= self.min.y && p.y <= self.max.y
&& p.z >= self.min.z && p.z <= self.max.z
}
pub fn intersects(&self, other: &AABB) -> bool {
self.min.x <= other.max.x && self.max.x >= other.min.x
&& self.min.y <= other.max.y && self.max.y >= other.min.y
&& self.min.z <= other.max.z && self.max.z >= other.min.z
}
pub fn merged(&self, other: &AABB) -> AABB {
AABB {
min: Vec3::new(
self.min.x.min(other.min.x),
self.min.y.min(other.min.y),
self.min.z.min(other.min.z),
),
max: Vec3::new(
self.max.x.max(other.max.x),
self.max.y.max(other.max.y),
self.max.z.max(other.max.z),
),
}
}
pub fn surface_area(&self) -> f32 {
let d = self.max - self.min;
2.0 * (d.x * d.y + d.y * d.z + d.z * d.x)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_and_accessors() {
let a = AABB::new(Vec3::new(-1.0, -2.0, -3.0), Vec3::new(1.0, 2.0, 3.0));
let c = a.center();
assert!((c.x).abs() < 1e-6);
assert!((c.y).abs() < 1e-6);
assert!((c.z).abs() < 1e-6);
let h = a.half_extents();
assert!((h.x - 1.0).abs() < 1e-6);
assert!((h.y - 2.0).abs() < 1e-6);
assert!((h.z - 3.0).abs() < 1e-6);
}
#[test]
fn test_from_center_half_extents() {
let a = AABB::from_center_half_extents(Vec3::new(5.0, 5.0, 5.0), Vec3::new(1.0, 1.0, 1.0));
assert_eq!(a.min, Vec3::new(4.0, 4.0, 4.0));
assert_eq!(a.max, Vec3::new(6.0, 6.0, 6.0));
}
#[test]
fn test_contains_point() {
let a = AABB::new(Vec3::ZERO, Vec3::new(2.0, 2.0, 2.0));
assert!(a.contains_point(Vec3::new(1.0, 1.0, 1.0)));
assert!(a.contains_point(Vec3::ZERO));
assert!(!a.contains_point(Vec3::new(3.0, 1.0, 1.0)));
}
#[test]
fn test_intersects() {
let a = AABB::new(Vec3::ZERO, Vec3::new(2.0, 2.0, 2.0));
let b = AABB::new(Vec3::new(1.0, 1.0, 1.0), Vec3::new(3.0, 3.0, 3.0));
assert!(a.intersects(&b));
let c = AABB::new(Vec3::new(5.0, 5.0, 5.0), Vec3::new(6.0, 6.0, 6.0));
assert!(!a.intersects(&c));
let d = AABB::new(Vec3::new(2.0, 0.0, 0.0), Vec3::new(3.0, 2.0, 2.0));
assert!(a.intersects(&d));
}
#[test]
fn test_merged() {
let a = AABB::new(Vec3::ZERO, Vec3::ONE);
let b = AABB::new(Vec3::new(2.0, 2.0, 2.0), Vec3::new(3.0, 3.0, 3.0));
let m = a.merged(&b);
assert_eq!(m.min, Vec3::ZERO);
assert_eq!(m.max, Vec3::new(3.0, 3.0, 3.0));
}
#[test]
fn test_surface_area() {
let a = AABB::new(Vec3::ZERO, Vec3::new(2.0, 2.0, 2.0));
assert!((a.surface_area() - 24.0).abs() < 1e-6);
}
}

View File

@@ -2,8 +2,12 @@ pub mod vec2;
pub mod vec3;
pub mod vec4;
pub mod mat4;
pub mod aabb;
pub mod ray;
pub use vec2::Vec2;
pub use vec3::Vec3;
pub use vec4::Vec4;
pub use mat4::Mat4;
pub use aabb::AABB;
pub use ray::Ray;

View File

@@ -0,0 +1,46 @@
use crate::Vec3;
#[derive(Debug, Clone, Copy)]
pub struct Ray {
pub origin: Vec3,
pub direction: Vec3,
}
impl Ray {
pub fn new(origin: Vec3, direction: Vec3) -> Self {
Self {
origin,
direction: direction.normalize(),
}
}
pub fn at(&self, t: f32) -> Vec3 {
self.origin + self.direction * t
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-5
}
fn approx_vec(a: Vec3, b: Vec3) -> bool {
approx(a.x, b.x) && approx(a.y, b.y) && approx(a.z, b.z)
}
#[test]
fn test_new_normalizes_direction() {
let r = Ray::new(Vec3::ZERO, Vec3::new(3.0, 0.0, 0.0));
assert!(approx_vec(r.direction, Vec3::X));
}
#[test]
fn test_at() {
let r = Ray::new(Vec3::new(1.0, 2.0, 3.0), Vec3::X);
let p = r.at(5.0);
assert!(approx_vec(p, Vec3::new(6.0, 2.0, 3.0)));
}
}

View File

@@ -0,0 +1,6 @@
[package]
name = "voltex_net"
version = "0.1.0"
edition = "2021"
[dependencies]

View File

@@ -0,0 +1,96 @@
use std::net::SocketAddr;
use crate::packet::Packet;
use crate::socket::NetSocket;
/// Events produced by the client during polling.
pub enum ClientEvent {
Connected { client_id: u32 },
Disconnected,
PacketReceived { packet: Packet },
}
/// A non-blocking UDP client.
pub struct NetClient {
socket: NetSocket,
server_addr: SocketAddr,
client_id: Option<u32>,
name: String,
}
impl NetClient {
/// Create and bind a new client socket.
///
/// - `local_addr`: local bind address (e.g. "127.0.0.1:0")
/// - `server_addr`: the server's `SocketAddr`
/// - `name`: client display name used in Connect packet
pub fn new(local_addr: &str, server_addr: SocketAddr, name: &str) -> Result<Self, String> {
let socket = NetSocket::bind(local_addr)?;
Ok(NetClient {
socket,
server_addr,
client_id: None,
name: name.to_string(),
})
}
/// Send a Connect packet to the server.
pub fn connect(&self) -> Result<(), String> {
let packet = Packet::Connect {
client_name: self.name.clone(),
};
self.socket.send_to(&packet, self.server_addr)
}
/// Poll for incoming packets and return any resulting client events.
pub fn poll(&mut self) -> Vec<ClientEvent> {
let mut events = Vec::new();
while let Some((packet, _addr)) = self.socket.recv_from() {
match &packet {
Packet::Accept { client_id } => {
self.client_id = Some(*client_id);
events.push(ClientEvent::Connected {
client_id: *client_id,
});
}
Packet::Disconnect { .. } => {
self.client_id = None;
events.push(ClientEvent::Disconnected);
}
_ => {
events.push(ClientEvent::PacketReceived {
packet: packet.clone(),
});
}
}
}
events
}
/// Send an arbitrary packet to the server.
pub fn send(&self, packet: Packet) -> Result<(), String> {
self.socket.send_to(&packet, self.server_addr)
}
/// Returns true if the client has received an Accept from the server.
pub fn is_connected(&self) -> bool {
self.client_id.is_some()
}
/// Returns the client id assigned by the server, or None if not yet connected.
pub fn client_id(&self) -> Option<u32> {
self.client_id
}
/// Send a Disconnect packet to the server and clear local state.
pub fn disconnect(&mut self) -> Result<(), String> {
if let Some(id) = self.client_id {
let packet = Packet::Disconnect { client_id: id };
self.socket.send_to(&packet, self.server_addr)?;
self.client_id = None;
}
Ok(())
}
}

View File

@@ -0,0 +1,9 @@
pub mod packet;
pub mod socket;
pub mod server;
pub mod client;
pub use packet::Packet;
pub use socket::NetSocket;
pub use server::{NetServer, ServerEvent, ClientInfo};
pub use client::{NetClient, ClientEvent};

View File

@@ -0,0 +1,210 @@
/// Packet type IDs
const TYPE_CONNECT: u8 = 1;
const TYPE_ACCEPT: u8 = 2;
const TYPE_DISCONNECT: u8 = 3;
const TYPE_PING: u8 = 4;
const TYPE_PONG: u8 = 5;
const TYPE_USER_DATA: u8 = 6;
/// Header size: type_id(1) + payload_len(2 LE) + reserved(1) = 4 bytes
const HEADER_SIZE: usize = 4;
/// All packet variants for the Voltex network protocol.
#[derive(Debug, Clone, PartialEq)]
pub enum Packet {
Connect { client_name: String },
Accept { client_id: u32 },
Disconnect { client_id: u32 },
Ping { timestamp: u64 },
Pong { timestamp: u64 },
UserData { client_id: u32, data: Vec<u8> },
}
impl Packet {
/// Serialize the packet into bytes: [type_id(1), payload_len(2 LE), reserved(1), payload...]
pub fn to_bytes(&self) -> Vec<u8> {
let payload = self.encode_payload();
let payload_len = payload.len() as u16;
let mut buf = Vec::with_capacity(HEADER_SIZE + payload.len());
buf.push(self.type_id());
buf.extend_from_slice(&payload_len.to_le_bytes());
buf.push(0u8); // reserved
buf.extend_from_slice(&payload);
buf
}
/// Deserialize a packet from bytes.
pub fn from_bytes(data: &[u8]) -> Result<Packet, String> {
if data.len() < HEADER_SIZE {
return Err(format!(
"Buffer too short for header: {} bytes",
data.len()
));
}
let type_id = data[0];
let payload_len = u16::from_le_bytes([data[1], data[2]]) as usize;
// data[3] is reserved, ignored
if data.len() < HEADER_SIZE + payload_len {
return Err(format!(
"Buffer too short: expected {} bytes, got {}",
HEADER_SIZE + payload_len,
data.len()
));
}
let payload = &data[HEADER_SIZE..HEADER_SIZE + payload_len];
match type_id {
TYPE_CONNECT => {
if payload.len() < 2 {
return Err("Connect payload too short".to_string());
}
let name_len = u16::from_le_bytes([payload[0], payload[1]]) as usize;
if payload.len() < 2 + name_len {
return Err("Connect name bytes too short".to_string());
}
let client_name = String::from_utf8(payload[2..2 + name_len].to_vec())
.map_err(|e| format!("Invalid UTF-8 in client_name: {}", e))?;
Ok(Packet::Connect { client_name })
}
TYPE_ACCEPT => {
if payload.len() < 4 {
return Err("Accept payload too short".to_string());
}
let client_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
Ok(Packet::Accept { client_id })
}
TYPE_DISCONNECT => {
if payload.len() < 4 {
return Err("Disconnect payload too short".to_string());
}
let client_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
Ok(Packet::Disconnect { client_id })
}
TYPE_PING => {
if payload.len() < 8 {
return Err("Ping payload too short".to_string());
}
let timestamp = u64::from_le_bytes([
payload[0], payload[1], payload[2], payload[3],
payload[4], payload[5], payload[6], payload[7],
]);
Ok(Packet::Ping { timestamp })
}
TYPE_PONG => {
if payload.len() < 8 {
return Err("Pong payload too short".to_string());
}
let timestamp = u64::from_le_bytes([
payload[0], payload[1], payload[2], payload[3],
payload[4], payload[5], payload[6], payload[7],
]);
Ok(Packet::Pong { timestamp })
}
TYPE_USER_DATA => {
if payload.len() < 4 {
return Err("UserData payload too short".to_string());
}
let client_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
let data = payload[4..].to_vec();
Ok(Packet::UserData { client_id, data })
}
_ => Err(format!("Unknown packet type_id: {}", type_id)),
}
}
fn type_id(&self) -> u8 {
match self {
Packet::Connect { .. } => TYPE_CONNECT,
Packet::Accept { .. } => TYPE_ACCEPT,
Packet::Disconnect { .. } => TYPE_DISCONNECT,
Packet::Ping { .. } => TYPE_PING,
Packet::Pong { .. } => TYPE_PONG,
Packet::UserData { .. } => TYPE_USER_DATA,
}
}
fn encode_payload(&self) -> Vec<u8> {
match self {
Packet::Connect { client_name } => {
let name_bytes = client_name.as_bytes();
let name_len = name_bytes.len() as u16;
let mut buf = Vec::with_capacity(2 + name_bytes.len());
buf.extend_from_slice(&name_len.to_le_bytes());
buf.extend_from_slice(name_bytes);
buf
}
Packet::Accept { client_id } => client_id.to_le_bytes().to_vec(),
Packet::Disconnect { client_id } => client_id.to_le_bytes().to_vec(),
Packet::Ping { timestamp } => timestamp.to_le_bytes().to_vec(),
Packet::Pong { timestamp } => timestamp.to_le_bytes().to_vec(),
Packet::UserData { client_id, data } => {
let mut buf = Vec::with_capacity(4 + data.len());
buf.extend_from_slice(&client_id.to_le_bytes());
buf.extend_from_slice(data);
buf
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(packet: Packet) {
let bytes = packet.to_bytes();
let decoded = Packet::from_bytes(&bytes).expect("roundtrip failed");
assert_eq!(packet, decoded);
}
#[test]
fn test_connect_roundtrip() {
roundtrip(Packet::Connect {
client_name: "Alice".to_string(),
});
}
#[test]
fn test_accept_roundtrip() {
roundtrip(Packet::Accept { client_id: 42 });
}
#[test]
fn test_disconnect_roundtrip() {
roundtrip(Packet::Disconnect { client_id: 7 });
}
#[test]
fn test_ping_roundtrip() {
roundtrip(Packet::Ping {
timestamp: 1_234_567_890_u64,
});
}
#[test]
fn test_pong_roundtrip() {
roundtrip(Packet::Pong {
timestamp: 9_876_543_210_u64,
});
}
#[test]
fn test_user_data_roundtrip() {
roundtrip(Packet::UserData {
client_id: 3,
data: vec![0xDE, 0xAD, 0xBE, 0xEF],
});
}
#[test]
fn test_invalid_type_returns_error() {
// Build a packet with type_id = 99 (unknown)
let bytes = vec![99u8, 0, 0, 0]; // type=99, payload_len=0, reserved=0
let result = Packet::from_bytes(&bytes);
assert!(result.is_err(), "Expected error for unknown type_id");
}
}

View File

@@ -0,0 +1,195 @@
use std::collections::HashMap;
use std::net::SocketAddr;
use crate::packet::Packet;
use crate::socket::NetSocket;
/// Information about a connected client.
pub struct ClientInfo {
pub id: u32,
pub addr: SocketAddr,
pub name: String,
}
/// Events produced by the server during polling.
pub enum ServerEvent {
ClientConnected { client_id: u32, name: String },
ClientDisconnected { client_id: u32 },
PacketReceived { client_id: u32, packet: Packet },
}
/// A non-blocking UDP server that manages multiple clients.
pub struct NetServer {
socket: NetSocket,
clients: HashMap<u32, ClientInfo>,
addr_to_id: HashMap<SocketAddr, u32>,
next_id: u32,
}
impl NetServer {
/// Bind the server to the given address.
pub fn new(addr: &str) -> Result<Self, String> {
let socket = NetSocket::bind(addr)?;
Ok(NetServer {
socket,
clients: HashMap::new(),
addr_to_id: HashMap::new(),
next_id: 1,
})
}
/// Return the local address the server is listening on.
pub fn local_addr(&self) -> SocketAddr {
self.socket.local_addr()
}
/// Poll for incoming packets and return any resulting server events.
pub fn poll(&mut self) -> Vec<ServerEvent> {
let mut events = Vec::new();
while let Some((packet, addr)) = self.socket.recv_from() {
match &packet {
Packet::Connect { client_name } => {
// Assign a new id and send Accept
let id = self.next_id;
self.next_id += 1;
let name = client_name.clone();
let info = ClientInfo {
id,
addr,
name: name.clone(),
};
self.clients.insert(id, info);
self.addr_to_id.insert(addr, id);
let accept = Packet::Accept { client_id: id };
if let Err(e) = self.socket.send_to(&accept, addr) {
eprintln!("[NetServer] Failed to send Accept to {}: {}", addr, e);
}
events.push(ServerEvent::ClientConnected { client_id: id, name });
}
Packet::Disconnect { client_id } => {
let id = *client_id;
if let Some(info) = self.clients.remove(&id) {
self.addr_to_id.remove(&info.addr);
events.push(ServerEvent::ClientDisconnected { client_id: id });
}
}
_ => {
// Map address to client id
if let Some(&client_id) = self.addr_to_id.get(&addr) {
events.push(ServerEvent::PacketReceived {
client_id,
packet: packet.clone(),
});
} else {
eprintln!("[NetServer] Packet from unknown addr {}", addr);
}
}
}
}
events
}
/// Send a packet to every connected client.
pub fn broadcast(&self, packet: &Packet) {
for info in self.clients.values() {
if let Err(e) = self.socket.send_to(packet, info.addr) {
eprintln!("[NetServer] broadcast failed for client {}: {}", info.id, e);
}
}
}
/// Send a packet to a specific client by id.
pub fn send_to_client(&self, id: u32, packet: &Packet) {
if let Some(info) = self.clients.get(&id) {
if let Err(e) = self.socket.send_to(packet, info.addr) {
eprintln!("[NetServer] send_to_client {} failed: {}", id, e);
}
}
}
/// Returns a slice of all connected clients.
pub fn clients(&self) -> impl Iterator<Item = &ClientInfo> {
self.clients.values()
}
/// Returns the number of connected clients.
pub fn client_count(&self) -> usize {
self.clients.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::{ClientEvent, NetClient};
use std::time::Duration;
#[test]
fn test_integration_connect_and_userdata() {
// Step 1: Start server on OS-assigned port
let mut server = NetServer::new("127.0.0.1:0").expect("server bind failed");
let server_addr = server.local_addr();
// Step 2: Create client on OS-assigned port, point at server
let mut client = NetClient::new("127.0.0.1:0", server_addr, "TestClient")
.expect("client bind failed");
// Step 3: Client sends Connect
client.connect().expect("connect send failed");
// Step 4: Give the packet time to travel
std::thread::sleep(Duration::from_millis(50));
// Step 5: Server poll → should get ClientConnected
let server_events = server.poll();
let mut connected_id = None;
for event in &server_events {
if let ServerEvent::ClientConnected { client_id, name } = event {
connected_id = Some(*client_id);
assert_eq!(name, "TestClient");
}
}
assert!(connected_id.is_some(), "Server did not receive ClientConnected");
// Step 6: Client poll → should get Connected
std::thread::sleep(Duration::from_millis(50));
let client_events = client.poll();
let mut got_connected = false;
for event in &client_events {
if let ClientEvent::Connected { client_id } = event {
assert_eq!(Some(*client_id), connected_id);
got_connected = true;
}
}
assert!(got_connected, "Client did not receive Connected event");
// Step 7: Client sends UserData, server should receive it
let cid = client.client_id().unwrap();
let user_packet = Packet::UserData {
client_id: cid,
data: vec![1, 2, 3, 4],
};
client.send(user_packet.clone()).expect("send userdata failed");
std::thread::sleep(Duration::from_millis(50));
let server_events2 = server.poll();
let mut got_packet = false;
for event in server_events2 {
if let ServerEvent::PacketReceived { client_id, packet } = event {
assert_eq!(client_id, cid);
assert_eq!(packet, user_packet);
got_packet = true;
}
}
assert!(got_packet, "Server did not receive UserData packet");
// Cleanup: disconnect
client.disconnect().expect("disconnect send failed");
}
}

View File

@@ -0,0 +1,58 @@
use std::net::{SocketAddr, UdpSocket};
use crate::Packet;
/// Maximum UDP datagram size we'll allocate for receiving.
const MAX_PACKET_SIZE: usize = 65535;
/// A non-blocking UDP socket wrapper.
pub struct NetSocket {
inner: UdpSocket,
}
impl NetSocket {
/// Bind a new non-blocking UDP socket to the given address.
pub fn bind(addr: &str) -> Result<Self, String> {
let socket = UdpSocket::bind(addr)
.map_err(|e| format!("UdpSocket::bind({}) failed: {}", addr, e))?;
socket
.set_nonblocking(true)
.map_err(|e| format!("set_nonblocking failed: {}", e))?;
Ok(NetSocket { inner: socket })
}
/// Returns the local address this socket is bound to.
pub fn local_addr(&self) -> SocketAddr {
self.inner.local_addr().expect("local_addr unavailable")
}
/// Serialize and send a packet to the given address.
pub fn send_to(&self, packet: &Packet, addr: SocketAddr) -> Result<(), String> {
let bytes = packet.to_bytes();
self.inner
.send_to(&bytes, addr)
.map_err(|e| format!("send_to failed: {}", e))?;
Ok(())
}
/// Try to receive one packet. Returns None on WouldBlock (no data available).
pub fn recv_from(&self) -> Option<(Packet, SocketAddr)> {
let mut buf = vec![0u8; MAX_PACKET_SIZE];
match self.inner.recv_from(&mut buf) {
Ok((len, addr)) => {
match Packet::from_bytes(&buf[..len]) {
Ok(packet) => Some((packet, addr)),
Err(e) => {
eprintln!("[NetSocket] Failed to parse packet from {}: {}", addr, e);
None
}
}
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => None,
Err(e) => {
eprintln!("[NetSocket] recv_from error: {}", e);
None
}
}
}
}

View File

@@ -0,0 +1,8 @@
[package]
name = "voltex_physics"
version = "0.1.0"
edition = "2021"
[dependencies]
voltex_math.workspace = true
voltex_ecs.workspace = true

View File

@@ -0,0 +1,166 @@
use voltex_ecs::Entity;
use voltex_math::AABB;
#[derive(Debug)]
enum BvhNode {
Leaf { entity: Entity, aabb: AABB },
Internal { aabb: AABB, left: usize, right: usize },
}
#[derive(Debug)]
pub struct BvhTree {
nodes: Vec<BvhNode>,
}
impl BvhTree {
pub fn build(entries: &[(Entity, AABB)]) -> Self {
let mut tree = BvhTree { nodes: Vec::new() };
if !entries.is_empty() {
let mut sorted: Vec<(Entity, AABB)> = entries.to_vec();
tree.build_recursive(&mut sorted);
}
tree
}
fn build_recursive(&mut self, entries: &mut [(Entity, AABB)]) -> usize {
if entries.len() == 1 {
let idx = self.nodes.len();
self.nodes.push(BvhNode::Leaf {
entity: entries[0].0,
aabb: entries[0].1,
});
return idx;
}
// Compute bounding AABB
let mut combined = entries[0].1;
for e in entries.iter().skip(1) {
combined = combined.merged(&e.1);
}
// Find longest axis
let extent = combined.max - combined.min;
let axis = if extent.x >= extent.y && extent.x >= extent.z {
0
} else if extent.y >= extent.z {
1
} else {
2
};
// Sort by axis center
entries.sort_by(|a, b| {
let ca = a.1.center();
let cb = b.1.center();
let va = match axis { 0 => ca.x, 1 => ca.y, _ => ca.z };
let vb = match axis { 0 => cb.x, 1 => cb.y, _ => cb.z };
va.partial_cmp(&vb).unwrap()
});
let mid = entries.len() / 2;
let (left_entries, right_entries) = entries.split_at_mut(mid);
let left = self.build_recursive(left_entries);
let right = self.build_recursive(right_entries);
let idx = self.nodes.len();
self.nodes.push(BvhNode::Internal {
aabb: combined,
left,
right,
});
idx
}
pub fn query_pairs(&self) -> Vec<(Entity, Entity)> {
let mut pairs = Vec::new();
if self.nodes.is_empty() {
return pairs;
}
let root = self.nodes.len() - 1;
let mut leaves = Vec::new();
self.collect_leaves(root, &mut leaves);
for i in 0..leaves.len() {
for j in (i + 1)..leaves.len() {
let (ea, aabb_a) = leaves[i];
let (eb, aabb_b) = leaves[j];
if aabb_a.intersects(&aabb_b) {
pairs.push((ea, eb));
}
}
}
pairs
}
fn collect_leaves(&self, node_idx: usize, out: &mut Vec<(Entity, AABB)>) {
match &self.nodes[node_idx] {
BvhNode::Leaf { entity, aabb } => {
out.push((*entity, *aabb));
}
BvhNode::Internal { left, right, .. } => {
self.collect_leaves(*left, out);
self.collect_leaves(*right, out);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use voltex_math::Vec3;
fn make_entity(id: u32) -> Entity {
Entity { id, generation: 0 }
}
#[test]
fn test_build_empty() {
let tree = BvhTree::build(&[]);
assert!(tree.query_pairs().is_empty());
}
#[test]
fn test_build_single() {
let entries = vec![
(make_entity(0), AABB::new(Vec3::ZERO, Vec3::ONE)),
];
let tree = BvhTree::build(&entries);
assert!(tree.query_pairs().is_empty());
}
#[test]
fn test_overlapping_pair() {
let entries = vec![
(make_entity(0), AABB::new(Vec3::ZERO, Vec3::new(2.0, 2.0, 2.0))),
(make_entity(1), AABB::new(Vec3::ONE, Vec3::new(3.0, 3.0, 3.0))),
];
let tree = BvhTree::build(&entries);
let pairs = tree.query_pairs();
assert_eq!(pairs.len(), 1);
}
#[test]
fn test_separated_pair() {
let entries = vec![
(make_entity(0), AABB::new(Vec3::ZERO, Vec3::ONE)),
(make_entity(1), AABB::new(Vec3::new(5.0, 5.0, 5.0), Vec3::new(6.0, 6.0, 6.0))),
];
let tree = BvhTree::build(&entries);
assert!(tree.query_pairs().is_empty());
}
#[test]
fn test_multiple_entities() {
let entries = vec![
(make_entity(0), AABB::new(Vec3::ZERO, Vec3::new(2.0, 2.0, 2.0))),
(make_entity(1), AABB::new(Vec3::ONE, Vec3::new(3.0, 3.0, 3.0))),
(make_entity(2), AABB::new(Vec3::new(10.0, 10.0, 10.0), Vec3::new(11.0, 11.0, 11.0))),
];
let tree = BvhTree::build(&entries);
let pairs = tree.query_pairs();
assert_eq!(pairs.len(), 1);
let (a, b) = pairs[0];
assert!((a.id == 0 && b.id == 1) || (a.id == 1 && b.id == 0));
}
}

View File

@@ -0,0 +1,42 @@
use voltex_math::{Vec3, AABB};
#[derive(Debug, Clone, Copy)]
pub enum Collider {
Sphere { radius: f32 },
Box { half_extents: Vec3 },
}
impl Collider {
pub fn aabb(&self, position: Vec3) -> AABB {
match self {
Collider::Sphere { radius } => {
let r = Vec3::new(*radius, *radius, *radius);
AABB::new(position - r, position + r)
}
Collider::Box { half_extents } => {
AABB::new(position - *half_extents, position + *half_extents)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sphere_aabb() {
let c = Collider::Sphere { radius: 2.0 };
let aabb = c.aabb(Vec3::new(1.0, 0.0, 0.0));
assert_eq!(aabb.min, Vec3::new(-1.0, -2.0, -2.0));
assert_eq!(aabb.max, Vec3::new(3.0, 2.0, 2.0));
}
#[test]
fn test_box_aabb() {
let c = Collider::Box { half_extents: Vec3::new(1.0, 2.0, 3.0) };
let aabb = c.aabb(Vec3::ZERO);
assert_eq!(aabb.min, Vec3::new(-1.0, -2.0, -3.0));
assert_eq!(aabb.max, Vec3::new(1.0, 2.0, 3.0));
}
}

View File

@@ -0,0 +1,181 @@
use voltex_ecs::{World, Entity};
use voltex_ecs::Transform;
use voltex_math::Vec3;
use crate::collider::Collider;
use crate::contact::ContactPoint;
use crate::bvh::BvhTree;
use crate::narrow;
pub fn detect_collisions(world: &World) -> Vec<ContactPoint> {
// 1. Gather entities with Transform + Collider
let pairs_data: Vec<(Entity, Vec3, Collider)> = world
.query2::<Transform, Collider>()
.into_iter()
.map(|(e, t, c)| (e, t.position, *c))
.collect();
if pairs_data.len() < 2 {
return Vec::new();
}
// 2. Build AABBs
let entries: Vec<(Entity, voltex_math::AABB)> = pairs_data
.iter()
.map(|(e, pos, col)| (*e, col.aabb(*pos)))
.collect();
// 3. Broad phase
let bvh = BvhTree::build(&entries);
let broad_pairs = bvh.query_pairs();
// 4. Narrow phase
let mut contacts = Vec::new();
let lookup = |entity: Entity| -> Option<(Vec3, Collider)> {
pairs_data.iter().find(|(e, _, _)| *e == entity).map(|(_, p, c)| (*p, *c))
};
for (ea, eb) in broad_pairs {
let (pos_a, col_a) = match lookup(ea) { Some(v) => v, None => continue };
let (pos_b, col_b) = match lookup(eb) { Some(v) => v, None => continue };
let result = match (&col_a, &col_b) {
(Collider::Sphere { radius: ra }, Collider::Sphere { radius: rb }) => {
narrow::sphere_vs_sphere(pos_a, *ra, pos_b, *rb)
}
(Collider::Sphere { radius }, Collider::Box { half_extents }) => {
narrow::sphere_vs_box(pos_a, *radius, pos_b, *half_extents)
}
(Collider::Box { half_extents }, Collider::Sphere { radius }) => {
narrow::sphere_vs_box(pos_b, *radius, pos_a, *half_extents)
.map(|(n, d, pa, pb)| (-n, d, pb, pa))
}
(Collider::Box { half_extents: ha }, Collider::Box { half_extents: hb }) => {
narrow::box_vs_box(pos_a, *ha, pos_b, *hb)
}
};
if let Some((normal, depth, point_on_a, point_on_b)) = result {
contacts.push(ContactPoint {
entity_a: ea,
entity_b: eb,
normal,
depth,
point_on_a,
point_on_b,
});
}
}
contacts
}
#[cfg(test)]
mod tests {
use super::*;
use voltex_ecs::World;
use voltex_ecs::Transform;
use voltex_math::Vec3;
use crate::Collider;
#[test]
fn test_no_colliders() {
let world = World::new();
let contacts = detect_collisions(&world);
assert!(contacts.is_empty());
}
#[test]
fn test_single_entity() {
let mut world = World::new();
let e = world.spawn();
world.add(e, Transform::from_position(Vec3::ZERO));
world.add(e, Collider::Sphere { radius: 1.0 });
let contacts = detect_collisions(&world);
assert!(contacts.is_empty());
}
#[test]
fn test_two_spheres_colliding() {
let mut world = World::new();
let a = world.spawn();
world.add(a, Transform::from_position(Vec3::ZERO));
world.add(a, Collider::Sphere { radius: 1.0 });
let b = world.spawn();
world.add(b, Transform::from_position(Vec3::new(1.5, 0.0, 0.0)));
world.add(b, Collider::Sphere { radius: 1.0 });
let contacts = detect_collisions(&world);
assert_eq!(contacts.len(), 1);
assert!((contacts[0].depth - 0.5).abs() < 1e-5);
}
#[test]
fn test_two_spheres_separated() {
let mut world = World::new();
let a = world.spawn();
world.add(a, Transform::from_position(Vec3::ZERO));
world.add(a, Collider::Sphere { radius: 1.0 });
let b = world.spawn();
world.add(b, Transform::from_position(Vec3::new(10.0, 0.0, 0.0)));
world.add(b, Collider::Sphere { radius: 1.0 });
let contacts = detect_collisions(&world);
assert!(contacts.is_empty());
}
#[test]
fn test_sphere_vs_box_collision() {
let mut world = World::new();
let a = world.spawn();
world.add(a, Transform::from_position(Vec3::ZERO));
world.add(a, Collider::Sphere { radius: 1.0 });
let b = world.spawn();
world.add(b, Transform::from_position(Vec3::new(1.5, 0.0, 0.0)));
world.add(b, Collider::Box { half_extents: Vec3::ONE });
let contacts = detect_collisions(&world);
assert_eq!(contacts.len(), 1);
assert!(contacts[0].depth > 0.0);
}
#[test]
fn test_box_vs_box_collision() {
let mut world = World::new();
let a = world.spawn();
world.add(a, Transform::from_position(Vec3::ZERO));
world.add(a, Collider::Box { half_extents: Vec3::ONE });
let b = world.spawn();
world.add(b, Transform::from_position(Vec3::new(1.5, 0.0, 0.0)));
world.add(b, Collider::Box { half_extents: Vec3::ONE });
let contacts = detect_collisions(&world);
assert_eq!(contacts.len(), 1);
assert!((contacts[0].depth - 0.5).abs() < 1e-5);
}
#[test]
fn test_three_entities_mixed() {
let mut world = World::new();
let a = world.spawn();
world.add(a, Transform::from_position(Vec3::ZERO));
world.add(a, Collider::Sphere { radius: 1.0 });
let b = world.spawn();
world.add(b, Transform::from_position(Vec3::new(1.5, 0.0, 0.0)));
world.add(b, Collider::Sphere { radius: 1.0 });
let c = world.spawn();
world.add(c, Transform::from_position(Vec3::new(100.0, 0.0, 0.0)));
world.add(c, Collider::Box { half_extents: Vec3::ONE });
let contacts = detect_collisions(&world);
assert_eq!(contacts.len(), 1);
}
}

View File

@@ -0,0 +1,12 @@
use voltex_ecs::Entity;
use voltex_math::Vec3;
#[derive(Debug, Clone, Copy)]
pub struct ContactPoint {
pub entity_a: Entity,
pub entity_b: Entity,
pub normal: Vec3,
pub depth: f32,
pub point_on_a: Vec3,
pub point_on_b: Vec3,
}

View File

@@ -0,0 +1,96 @@
use voltex_ecs::World;
use voltex_ecs::Transform;
use voltex_math::Vec3;
use crate::rigid_body::{RigidBody, PhysicsConfig};
pub fn integrate(world: &mut World, config: &PhysicsConfig) {
// 1. Collect
let updates: Vec<(voltex_ecs::Entity, Vec3, Vec3)> = world
.query2::<Transform, RigidBody>()
.into_iter()
.filter(|(_, _, rb)| !rb.is_static())
.map(|(entity, transform, rb)| {
let new_velocity = rb.velocity + config.gravity * rb.gravity_scale * config.fixed_dt;
let new_position = transform.position + new_velocity * config.fixed_dt;
(entity, new_velocity, new_position)
})
.collect();
// 2. Apply
for (entity, new_velocity, new_position) in updates {
if let Some(rb) = world.get_mut::<RigidBody>(entity) {
rb.velocity = new_velocity;
}
if let Some(t) = world.get_mut::<Transform>(entity) {
t.position = new_position;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use voltex_ecs::World;
use voltex_ecs::Transform;
use voltex_math::Vec3;
use crate::RigidBody;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-4
}
#[test]
fn test_gravity_fall() {
let mut world = World::new();
let e = world.spawn();
world.add(e, Transform::from_position(Vec3::new(0.0, 10.0, 0.0)));
world.add(e, RigidBody::dynamic(1.0));
let config = PhysicsConfig::default();
integrate(&mut world, &config);
let rb = world.get::<RigidBody>(e).unwrap();
let t = world.get::<Transform>(e).unwrap();
let expected_vy = -9.81 * config.fixed_dt;
assert!(approx(rb.velocity.y, expected_vy));
let expected_py = 10.0 + expected_vy * config.fixed_dt;
assert!(approx(t.position.y, expected_py));
}
#[test]
fn test_static_unchanged() {
let mut world = World::new();
let e = world.spawn();
world.add(e, Transform::from_position(Vec3::new(0.0, 5.0, 0.0)));
world.add(e, RigidBody::statik());
let config = PhysicsConfig::default();
integrate(&mut world, &config);
let t = world.get::<Transform>(e).unwrap();
assert!(approx(t.position.y, 5.0));
let rb = world.get::<RigidBody>(e).unwrap();
assert!(approx(rb.velocity.y, 0.0));
}
#[test]
fn test_initial_velocity() {
let mut world = World::new();
let e = world.spawn();
world.add(e, Transform::from_position(Vec3::ZERO));
let mut rb = RigidBody::dynamic(1.0);
rb.velocity = Vec3::new(5.0, 0.0, 0.0);
rb.gravity_scale = 0.0;
world.add(e, rb);
let config = PhysicsConfig::default();
integrate(&mut world, &config);
let t = world.get::<Transform>(e).unwrap();
let expected_x = 5.0 * config.fixed_dt;
assert!(approx(t.position.x, expected_x));
}
}

View File

@@ -0,0 +1,19 @@
pub mod bvh;
pub mod ray;
pub mod collider;
pub mod contact;
pub mod narrow;
pub mod collision;
pub mod rigid_body;
pub mod integrator;
pub mod solver;
pub mod raycast;
pub use bvh::BvhTree;
pub use collider::Collider;
pub use contact::ContactPoint;
pub use collision::detect_collisions;
pub use rigid_body::{RigidBody, PhysicsConfig};
pub use integrator::integrate;
pub use solver::{resolve_collisions, physics_step};
pub use raycast::{RayHit, raycast};

View File

@@ -0,0 +1,236 @@
use voltex_math::Vec3;
/// Returns (normal A→B, depth, point_on_a, point_on_b) or None if no collision.
pub fn sphere_vs_sphere(
pos_a: Vec3, radius_a: f32,
pos_b: Vec3, radius_b: f32,
) -> Option<(Vec3, f32, Vec3, Vec3)> {
let diff = pos_b - pos_a;
let dist_sq = diff.length_squared();
let sum_r = radius_a + radius_b;
if dist_sq > sum_r * sum_r {
return None;
}
let dist = dist_sq.sqrt();
let normal = if dist > 1e-8 {
diff * (1.0 / dist)
} else {
Vec3::Y
};
let depth = sum_r - dist;
let point_on_a = pos_a + normal * radius_a;
let point_on_b = pos_b - normal * radius_b;
Some((normal, depth, point_on_a, point_on_b))
}
pub fn sphere_vs_box(
sphere_pos: Vec3, radius: f32,
box_pos: Vec3, half_extents: Vec3,
) -> Option<(Vec3, f32, Vec3, Vec3)> {
let bmin = box_pos - half_extents;
let bmax = box_pos + half_extents;
let closest = Vec3::new(
sphere_pos.x.clamp(bmin.x, bmax.x),
sphere_pos.y.clamp(bmin.y, bmax.y),
sphere_pos.z.clamp(bmin.z, bmax.z),
);
let diff = sphere_pos - closest;
let dist_sq = diff.length_squared();
// Sphere center outside box
if dist_sq > 1e-8 {
let dist = dist_sq.sqrt();
if dist > radius {
return None;
}
let normal = diff * (-1.0 / dist); // sphere→box direction
let depth = radius - dist;
let point_on_a = sphere_pos + normal * radius; // sphere surface toward box
let point_on_b = closest;
return Some((normal, depth, point_on_a, point_on_b));
}
// Sphere center inside box — find nearest face
let dx_min = sphere_pos.x - bmin.x;
let dx_max = bmax.x - sphere_pos.x;
let dy_min = sphere_pos.y - bmin.y;
let dy_max = bmax.y - sphere_pos.y;
let dz_min = sphere_pos.z - bmin.z;
let dz_max = bmax.z - sphere_pos.z;
let mut min_dist = dx_min;
let mut normal = Vec3::new(-1.0, 0.0, 0.0);
let mut closest_face = Vec3::new(bmin.x, sphere_pos.y, sphere_pos.z);
if dx_max < min_dist {
min_dist = dx_max;
normal = Vec3::new(1.0, 0.0, 0.0);
closest_face = Vec3::new(bmax.x, sphere_pos.y, sphere_pos.z);
}
if dy_min < min_dist {
min_dist = dy_min;
normal = Vec3::new(0.0, -1.0, 0.0);
closest_face = Vec3::new(sphere_pos.x, bmin.y, sphere_pos.z);
}
if dy_max < min_dist {
min_dist = dy_max;
normal = Vec3::new(0.0, 1.0, 0.0);
closest_face = Vec3::new(sphere_pos.x, bmax.y, sphere_pos.z);
}
if dz_min < min_dist {
min_dist = dz_min;
normal = Vec3::new(0.0, 0.0, -1.0);
closest_face = Vec3::new(sphere_pos.x, sphere_pos.y, bmin.z);
}
if dz_max < min_dist {
min_dist = dz_max;
normal = Vec3::new(0.0, 0.0, 1.0);
closest_face = Vec3::new(sphere_pos.x, sphere_pos.y, bmax.z);
}
let depth = min_dist + radius;
let point_on_a = sphere_pos + normal * radius;
let point_on_b = closest_face;
Some((normal, depth, point_on_a, point_on_b))
}
pub fn box_vs_box(
pos_a: Vec3, half_a: Vec3,
pos_b: Vec3, half_b: Vec3,
) -> Option<(Vec3, f32, Vec3, Vec3)> {
let diff = pos_b - pos_a;
let overlap_x = (half_a.x + half_b.x) - diff.x.abs();
if overlap_x < 0.0 { return None; }
let overlap_y = (half_a.y + half_b.y) - diff.y.abs();
if overlap_y < 0.0 { return None; }
let overlap_z = (half_a.z + half_b.z) - diff.z.abs();
if overlap_z < 0.0 { return None; }
let (normal, depth) = if overlap_x <= overlap_y && overlap_x <= overlap_z {
let sign = if diff.x >= 0.0 { 1.0 } else { -1.0 };
(Vec3::new(sign, 0.0, 0.0), overlap_x)
} else if overlap_y <= overlap_z {
let sign = if diff.y >= 0.0 { 1.0 } else { -1.0 };
(Vec3::new(0.0, sign, 0.0), overlap_y)
} else {
let sign = if diff.z >= 0.0 { 1.0 } else { -1.0 };
(Vec3::new(0.0, 0.0, sign), overlap_z)
};
let point_on_a = pos_a + Vec3::new(
normal.x * half_a.x,
normal.y * half_a.y,
normal.z * half_a.z,
);
let point_on_b = pos_b - Vec3::new(
normal.x * half_b.x,
normal.y * half_b.y,
normal.z * half_b.z,
);
Some((normal, depth, point_on_a, point_on_b))
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) -> bool { (a - b).abs() < 1e-5 }
fn approx_vec(a: Vec3, b: Vec3) -> bool { approx(a.x, b.x) && approx(a.y, b.y) && approx(a.z, b.z) }
// sphere_vs_sphere tests
#[test]
fn test_sphere_sphere_separated() {
let r = sphere_vs_sphere(Vec3::ZERO, 1.0, Vec3::new(5.0, 0.0, 0.0), 1.0);
assert!(r.is_none());
}
#[test]
fn test_sphere_sphere_overlapping() {
let r = sphere_vs_sphere(Vec3::ZERO, 1.0, Vec3::new(1.5, 0.0, 0.0), 1.0);
let (normal, depth, pa, pb) = r.unwrap();
assert!(approx_vec(normal, Vec3::X));
assert!(approx(depth, 0.5));
assert!(approx_vec(pa, Vec3::new(1.0, 0.0, 0.0)));
assert!(approx_vec(pb, Vec3::new(0.5, 0.0, 0.0)));
}
#[test]
fn test_sphere_sphere_touching() {
let r = sphere_vs_sphere(Vec3::ZERO, 1.0, Vec3::new(2.0, 0.0, 0.0), 1.0);
let (normal, depth, _pa, _pb) = r.unwrap();
assert!(approx_vec(normal, Vec3::X));
assert!(approx(depth, 0.0));
}
#[test]
fn test_sphere_sphere_coincident() {
let r = sphere_vs_sphere(Vec3::ZERO, 1.0, Vec3::ZERO, 1.0);
let (_normal, depth, _pa, _pb) = r.unwrap();
assert!(approx(depth, 2.0));
}
// sphere_vs_box tests
#[test]
fn test_sphere_box_separated() {
let r = sphere_vs_box(Vec3::new(5.0, 0.0, 0.0), 1.0, Vec3::ZERO, Vec3::ONE);
assert!(r.is_none());
}
#[test]
fn test_sphere_box_face_overlap() {
let r = sphere_vs_box(Vec3::new(1.5, 0.0, 0.0), 1.0, Vec3::ZERO, Vec3::ONE);
let (normal, depth, _pa, pb) = r.unwrap();
assert!(approx(normal.x, -1.0));
assert!(approx(depth, 0.5));
assert!(approx(pb.x, 1.0));
}
#[test]
fn test_sphere_box_center_inside() {
let r = sphere_vs_box(Vec3::ZERO, 0.5, Vec3::ZERO, Vec3::ONE);
assert!(r.is_some());
let (_normal, depth, _pa, _pb) = r.unwrap();
assert!(depth > 0.0);
}
// box_vs_box tests
#[test]
fn test_box_box_separated() {
let r = box_vs_box(Vec3::ZERO, Vec3::ONE, Vec3::new(5.0, 0.0, 0.0), Vec3::ONE);
assert!(r.is_none());
}
#[test]
fn test_box_box_overlapping() {
let r = box_vs_box(Vec3::ZERO, Vec3::ONE, Vec3::new(1.5, 0.0, 0.0), Vec3::ONE);
let (normal, depth, _pa, _pb) = r.unwrap();
assert!(approx_vec(normal, Vec3::X));
assert!(approx(depth, 0.5));
}
#[test]
fn test_box_box_touching() {
let r = box_vs_box(Vec3::ZERO, Vec3::ONE, Vec3::new(2.0, 0.0, 0.0), Vec3::ONE);
let (_normal, depth, _pa, _pb) = r.unwrap();
assert!(approx(depth, 0.0));
}
#[test]
fn test_box_box_y_axis() {
let r = box_vs_box(Vec3::ZERO, Vec3::ONE, Vec3::new(0.0, 1.5, 0.0), Vec3::ONE);
let (normal, depth, _pa, _pb) = r.unwrap();
assert!(approx_vec(normal, Vec3::Y));
assert!(approx(depth, 0.5));
}
}

View File

@@ -0,0 +1,204 @@
use voltex_math::{Vec3, Ray, AABB};
/// Ray vs AABB (slab method). Returns t of nearest intersection, or None.
/// If ray starts inside AABB, returns Some(0.0).
pub fn ray_vs_aabb(ray: &Ray, aabb: &AABB) -> Option<f32> {
let mut t_min = f32::NEG_INFINITY;
let mut t_max = f32::INFINITY;
let o = [ray.origin.x, ray.origin.y, ray.origin.z];
let d = [ray.direction.x, ray.direction.y, ray.direction.z];
let bmin = [aabb.min.x, aabb.min.y, aabb.min.z];
let bmax = [aabb.max.x, aabb.max.y, aabb.max.z];
for i in 0..3 {
if d[i].abs() < 1e-8 {
if o[i] < bmin[i] || o[i] > bmax[i] {
return None;
}
} else {
let inv_d = 1.0 / d[i];
let mut t1 = (bmin[i] - o[i]) * inv_d;
let mut t2 = (bmax[i] - o[i]) * inv_d;
if t1 > t2 {
std::mem::swap(&mut t1, &mut t2);
}
t_min = t_min.max(t1);
t_max = t_max.min(t2);
if t_min > t_max {
return None;
}
}
}
if t_max < 0.0 {
return None;
}
Some(t_min.max(0.0))
}
/// Ray vs Sphere. Returns (t, normal) or None.
pub fn ray_vs_sphere(ray: &Ray, center: Vec3, radius: f32) -> Option<(f32, Vec3)> {
let oc = ray.origin - center;
let a = ray.direction.dot(ray.direction);
let b = 2.0 * oc.dot(ray.direction);
let c = oc.dot(oc) - radius * radius;
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
return None;
}
let sqrt_d = discriminant.sqrt();
let mut t = (-b - sqrt_d) / (2.0 * a);
if t < 0.0 {
t = (-b + sqrt_d) / (2.0 * a);
if t < 0.0 {
return None;
}
}
let point = ray.at(t);
let normal = (point - center).normalize();
Some((t, normal))
}
/// Ray vs axis-aligned Box. Returns (t, normal) or None.
pub fn ray_vs_box(ray: &Ray, center: Vec3, half_extents: Vec3) -> Option<(f32, Vec3)> {
let aabb = AABB::from_center_half_extents(center, half_extents);
let t = ray_vs_aabb(ray, &aabb)?;
if t == 0.0 {
// Ray starts inside box
let bmin = aabb.min;
let bmax = aabb.max;
let p = ray.origin;
let faces: [(f32, Vec3); 6] = [
(p.x - bmin.x, Vec3::new(-1.0, 0.0, 0.0)),
(bmax.x - p.x, Vec3::new(1.0, 0.0, 0.0)),
(p.y - bmin.y, Vec3::new(0.0, -1.0, 0.0)),
(bmax.y - p.y, Vec3::new(0.0, 1.0, 0.0)),
(p.z - bmin.z, Vec3::new(0.0, 0.0, -1.0)),
(bmax.z - p.z, Vec3::new(0.0, 0.0, 1.0)),
];
let mut min_dist = f32::INFINITY;
let mut normal = Vec3::Y;
for (dist, n) in &faces {
if *dist < min_dist {
min_dist = *dist;
normal = *n;
}
}
return Some((0.0, normal));
}
let hit = ray.at(t);
let rel = hit - center;
let hx = half_extents.x;
let hy = half_extents.y;
let hz = half_extents.z;
let normal = if (rel.x - hx).abs() < 1e-4 {
Vec3::X
} else if (rel.x + hx).abs() < 1e-4 {
Vec3::new(-1.0, 0.0, 0.0)
} else if (rel.y - hy).abs() < 1e-4 {
Vec3::Y
} else if (rel.y + hy).abs() < 1e-4 {
Vec3::new(0.0, -1.0, 0.0)
} else if (rel.z - hz).abs() < 1e-4 {
Vec3::Z
} else {
Vec3::new(0.0, 0.0, -1.0)
};
Some((t, normal))
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-4
}
fn approx_vec(a: Vec3, b: Vec3) -> bool {
approx(a.x, b.x) && approx(a.y, b.y) && approx(a.z, b.z)
}
#[test]
fn test_aabb_hit() {
let ray = Ray::new(Vec3::new(-5.0, 0.0, 0.0), Vec3::X);
let aabb = AABB::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::ONE);
let t = ray_vs_aabb(&ray, &aabb).unwrap();
assert!(approx(t, 4.0));
}
#[test]
fn test_aabb_miss() {
let ray = Ray::new(Vec3::new(-5.0, 5.0, 0.0), Vec3::X);
let aabb = AABB::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::ONE);
assert!(ray_vs_aabb(&ray, &aabb).is_none());
}
#[test]
fn test_aabb_inside() {
let ray = Ray::new(Vec3::ZERO, Vec3::X);
let aabb = AABB::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::ONE);
let t = ray_vs_aabb(&ray, &aabb).unwrap();
assert!(approx(t, 0.0));
}
#[test]
fn test_sphere_hit() {
let ray = Ray::new(Vec3::new(-5.0, 0.0, 0.0), Vec3::X);
let (t, normal) = ray_vs_sphere(&ray, Vec3::ZERO, 1.0).unwrap();
assert!(approx(t, 4.0));
assert!(approx_vec(normal, Vec3::new(-1.0, 0.0, 0.0)));
}
#[test]
fn test_sphere_miss() {
let ray = Ray::new(Vec3::new(-5.0, 5.0, 0.0), Vec3::X);
assert!(ray_vs_sphere(&ray, Vec3::ZERO, 1.0).is_none());
}
#[test]
fn test_sphere_tangent() {
let ray = Ray::new(Vec3::new(-5.0, 1.0, 0.0), Vec3::X);
assert!(ray_vs_sphere(&ray, Vec3::ZERO, 1.0).is_some());
}
#[test]
fn test_sphere_inside() {
let ray = Ray::new(Vec3::ZERO, Vec3::X);
let (t, _normal) = ray_vs_sphere(&ray, Vec3::ZERO, 2.0).unwrap();
assert!(approx(t, 2.0));
}
#[test]
fn test_box_hit_face() {
let ray = Ray::new(Vec3::new(-5.0, 0.0, 0.0), Vec3::X);
let (t, normal) = ray_vs_box(&ray, Vec3::ZERO, Vec3::ONE).unwrap();
assert!(approx(t, 4.0));
assert!(approx_vec(normal, Vec3::new(-1.0, 0.0, 0.0)));
}
#[test]
fn test_box_miss() {
let ray = Ray::new(Vec3::new(-5.0, 5.0, 0.0), Vec3::X);
assert!(ray_vs_box(&ray, Vec3::ZERO, Vec3::ONE).is_none());
}
#[test]
fn test_box_inside() {
let ray = Ray::new(Vec3::ZERO, Vec3::X);
let (t, _normal) = ray_vs_box(&ray, Vec3::ZERO, Vec3::ONE).unwrap();
assert!(approx(t, 0.0));
}
}

View File

@@ -0,0 +1,168 @@
use voltex_ecs::{World, Entity};
use voltex_ecs::Transform;
use voltex_math::{Vec3, Ray};
use crate::collider::Collider;
use crate::ray as ray_tests;
#[derive(Debug, Clone, Copy)]
pub struct RayHit {
pub entity: Entity,
pub t: f32,
pub point: Vec3,
pub normal: Vec3,
}
pub fn raycast(world: &World, ray: &Ray, max_dist: f32) -> Option<RayHit> {
let entities: Vec<(Entity, Vec3, Collider)> = world
.query2::<Transform, Collider>()
.into_iter()
.map(|(e, t, c)| (e, t.position, *c))
.collect();
if entities.is_empty() {
return None;
}
let mut closest: Option<RayHit> = None;
for (entity, pos, collider) in &entities {
let aabb = collider.aabb(*pos);
// Broad phase: ray vs AABB
let aabb_t = match ray_tests::ray_vs_aabb(ray, &aabb) {
Some(t) if t <= max_dist => t,
_ => continue,
};
// Early skip if we already have a closer hit
if let Some(ref hit) = closest {
if aabb_t >= hit.t {
continue;
}
}
// Narrow phase
let result = match collider {
Collider::Sphere { radius } => {
ray_tests::ray_vs_sphere(ray, *pos, *radius)
}
Collider::Box { half_extents } => {
ray_tests::ray_vs_box(ray, *pos, *half_extents)
}
};
if let Some((t, normal)) = result {
if t <= max_dist {
if closest.is_none() || t < closest.as_ref().unwrap().t {
closest = Some(RayHit {
entity: *entity,
t,
point: ray.at(t),
normal,
});
}
}
}
}
closest
}
#[cfg(test)]
mod tests {
use super::*;
use voltex_ecs::World;
use voltex_ecs::Transform;
use voltex_math::Vec3;
use crate::Collider;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-3
}
#[test]
fn test_empty_world() {
let world = World::new();
let ray = Ray::new(Vec3::ZERO, Vec3::X);
assert!(raycast(&world, &ray, 100.0).is_none());
}
#[test]
fn test_hit_sphere() {
let mut world = World::new();
let e = world.spawn();
world.add(e, Transform::from_position(Vec3::new(5.0, 0.0, 0.0)));
world.add(e, Collider::Sphere { radius: 1.0 });
let ray = Ray::new(Vec3::ZERO, Vec3::X);
let hit = raycast(&world, &ray, 100.0).unwrap();
assert_eq!(hit.entity, e);
assert!(approx(hit.t, 4.0));
assert!(approx(hit.point.x, 4.0));
}
#[test]
fn test_closest_of_multiple() {
let mut world = World::new();
let far = world.spawn();
world.add(far, Transform::from_position(Vec3::new(10.0, 0.0, 0.0)));
world.add(far, Collider::Sphere { radius: 1.0 });
let near = world.spawn();
world.add(near, Transform::from_position(Vec3::new(3.0, 0.0, 0.0)));
world.add(near, Collider::Sphere { radius: 1.0 });
let ray = Ray::new(Vec3::ZERO, Vec3::X);
let hit = raycast(&world, &ray, 100.0).unwrap();
assert_eq!(hit.entity, near);
assert!(approx(hit.t, 2.0));
}
#[test]
fn test_max_dist() {
let mut world = World::new();
let e = world.spawn();
world.add(e, Transform::from_position(Vec3::new(50.0, 0.0, 0.0)));
world.add(e, Collider::Sphere { radius: 1.0 });
let ray = Ray::new(Vec3::ZERO, Vec3::X);
assert!(raycast(&world, &ray, 10.0).is_none());
}
#[test]
fn test_hit_box() {
let mut world = World::new();
let e = world.spawn();
world.add(e, Transform::from_position(Vec3::new(5.0, 0.0, 0.0)));
world.add(e, Collider::Box { half_extents: Vec3::ONE });
let ray = Ray::new(Vec3::ZERO, Vec3::X);
let hit = raycast(&world, &ray, 100.0).unwrap();
assert_eq!(hit.entity, e);
assert!(approx(hit.t, 4.0));
}
#[test]
fn test_mixed_sphere_box() {
let mut world = World::new();
let sphere = world.spawn();
world.add(sphere, Transform::from_position(Vec3::new(10.0, 0.0, 0.0)));
world.add(sphere, Collider::Sphere { radius: 1.0 });
let box_e = world.spawn();
world.add(box_e, Transform::from_position(Vec3::new(3.0, 0.0, 0.0)));
world.add(box_e, Collider::Box { half_extents: Vec3::ONE });
let ray = Ray::new(Vec3::ZERO, Vec3::X);
let hit = raycast(&world, &ray, 100.0).unwrap();
assert_eq!(hit.entity, box_e);
assert!(approx(hit.t, 2.0));
}
}

View File

@@ -0,0 +1,86 @@
use voltex_math::Vec3;
#[derive(Debug, Clone, Copy)]
pub struct RigidBody {
pub velocity: Vec3,
pub angular_velocity: Vec3,
pub mass: f32,
pub restitution: f32,
pub gravity_scale: f32,
}
impl RigidBody {
pub fn dynamic(mass: f32) -> Self {
Self {
velocity: Vec3::ZERO,
angular_velocity: Vec3::ZERO,
mass,
restitution: 0.3,
gravity_scale: 1.0,
}
}
pub fn statik() -> Self {
Self {
velocity: Vec3::ZERO,
angular_velocity: Vec3::ZERO,
mass: 0.0,
restitution: 0.3,
gravity_scale: 0.0,
}
}
pub fn inv_mass(&self) -> f32 {
if self.mass == 0.0 { 0.0 } else { 1.0 / self.mass }
}
pub fn is_static(&self) -> bool {
self.mass == 0.0
}
}
pub struct PhysicsConfig {
pub gravity: Vec3,
pub fixed_dt: f32,
}
impl Default for PhysicsConfig {
fn default() -> Self {
Self {
gravity: Vec3::new(0.0, -9.81, 0.0),
fixed_dt: 1.0 / 60.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dynamic_body() {
let rb = RigidBody::dynamic(2.0);
assert_eq!(rb.mass, 2.0);
assert!(!rb.is_static());
assert!((rb.inv_mass() - 0.5).abs() < 1e-6);
assert_eq!(rb.velocity, Vec3::ZERO);
assert_eq!(rb.restitution, 0.3);
assert_eq!(rb.gravity_scale, 1.0);
}
#[test]
fn test_static_body() {
let rb = RigidBody::statik();
assert_eq!(rb.mass, 0.0);
assert!(rb.is_static());
assert_eq!(rb.inv_mass(), 0.0);
assert_eq!(rb.gravity_scale, 0.0);
}
#[test]
fn test_physics_config_default() {
let cfg = PhysicsConfig::default();
assert!((cfg.gravity.y - (-9.81)).abs() < 1e-6);
assert!((cfg.fixed_dt - 1.0 / 60.0).abs() < 1e-6);
}
}

View File

@@ -0,0 +1,227 @@
use voltex_ecs::{World, Entity};
use voltex_ecs::Transform;
use voltex_math::Vec3;
use crate::contact::ContactPoint;
use crate::rigid_body::{RigidBody, PhysicsConfig};
use crate::collision::detect_collisions;
use crate::integrator::integrate;
const POSITION_SLOP: f32 = 0.01;
const POSITION_PERCENT: f32 = 0.4;
pub fn resolve_collisions(world: &mut World, contacts: &[ContactPoint]) {
let mut velocity_changes: Vec<(Entity, Vec3)> = Vec::new();
let mut position_changes: Vec<(Entity, Vec3)> = Vec::new();
for contact in contacts {
let rb_a = world.get::<RigidBody>(contact.entity_a).copied();
let rb_b = world.get::<RigidBody>(contact.entity_b).copied();
let (rb_a, rb_b) = match (rb_a, rb_b) {
(Some(a), Some(b)) => (a, b),
_ => continue,
};
let inv_mass_a = rb_a.inv_mass();
let inv_mass_b = rb_b.inv_mass();
let inv_mass_sum = inv_mass_a + inv_mass_b;
if inv_mass_sum == 0.0 {
continue;
}
let v_rel = rb_a.velocity - rb_b.velocity;
let v_rel_n = v_rel.dot(contact.normal);
// normal points A→B; v_rel_n > 0 means A approaches B → apply impulse
if v_rel_n > 0.0 {
let e = rb_a.restitution.min(rb_b.restitution);
let j = (1.0 + e) * v_rel_n / inv_mass_sum;
velocity_changes.push((contact.entity_a, contact.normal * (-j * inv_mass_a)));
velocity_changes.push((contact.entity_b, contact.normal * (j * inv_mass_b)));
}
let correction_mag = (contact.depth - POSITION_SLOP).max(0.0) * POSITION_PERCENT / inv_mass_sum;
if correction_mag > 0.0 {
let correction = contact.normal * correction_mag;
position_changes.push((contact.entity_a, correction * (-inv_mass_a)));
position_changes.push((contact.entity_b, correction * inv_mass_b));
}
}
for (entity, dv) in velocity_changes {
if let Some(rb) = world.get_mut::<RigidBody>(entity) {
rb.velocity = rb.velocity + dv;
}
}
for (entity, dp) in position_changes {
if let Some(t) = world.get_mut::<Transform>(entity) {
t.position = t.position + dp;
}
}
}
pub fn physics_step(world: &mut World, config: &PhysicsConfig) {
integrate(world, config);
let contacts = detect_collisions(world);
resolve_collisions(world, &contacts);
}
#[cfg(test)]
mod tests {
use super::*;
use voltex_ecs::World;
use voltex_ecs::Transform;
use voltex_math::Vec3;
use crate::{Collider, RigidBody};
use crate::collision::detect_collisions;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-3
}
#[test]
fn test_two_dynamic_spheres_head_on() {
let mut world = World::new();
let a = world.spawn();
world.add(a, Transform::from_position(Vec3::new(-0.5, 0.0, 0.0)));
world.add(a, Collider::Sphere { radius: 1.0 });
let mut rb_a = RigidBody::dynamic(1.0);
rb_a.velocity = Vec3::new(1.0, 0.0, 0.0);
rb_a.restitution = 1.0;
rb_a.gravity_scale = 0.0;
world.add(a, rb_a);
let b = world.spawn();
world.add(b, Transform::from_position(Vec3::new(0.5, 0.0, 0.0)));
world.add(b, Collider::Sphere { radius: 1.0 });
let mut rb_b = RigidBody::dynamic(1.0);
rb_b.velocity = Vec3::new(-1.0, 0.0, 0.0);
rb_b.restitution = 1.0;
rb_b.gravity_scale = 0.0;
world.add(b, rb_b);
let contacts = detect_collisions(&world);
assert_eq!(contacts.len(), 1);
resolve_collisions(&mut world, &contacts);
let va = world.get::<RigidBody>(a).unwrap().velocity;
let vb = world.get::<RigidBody>(b).unwrap().velocity;
assert!(approx(va.x, -1.0));
assert!(approx(vb.x, 1.0));
}
#[test]
fn test_dynamic_vs_static_floor() {
let mut world = World::new();
let ball = world.spawn();
world.add(ball, Transform::from_position(Vec3::new(0.0, 0.5, 0.0)));
world.add(ball, Collider::Sphere { radius: 1.0 });
let mut rb = RigidBody::dynamic(1.0);
rb.velocity = Vec3::new(0.0, -2.0, 0.0);
rb.restitution = 1.0;
rb.gravity_scale = 0.0;
world.add(ball, rb);
let floor = world.spawn();
world.add(floor, Transform::from_position(Vec3::new(0.0, -1.0, 0.0)));
world.add(floor, Collider::Box { half_extents: Vec3::new(10.0, 1.0, 10.0) });
world.add(floor, RigidBody::statik());
let contacts = detect_collisions(&world);
assert_eq!(contacts.len(), 1);
resolve_collisions(&mut world, &contacts);
let ball_rb = world.get::<RigidBody>(ball).unwrap();
let floor_rb = world.get::<RigidBody>(floor).unwrap();
assert!(ball_rb.velocity.y > 0.0);
assert!(approx(floor_rb.velocity.y, 0.0));
}
#[test]
fn test_position_correction() {
let mut world = World::new();
let a = world.spawn();
world.add(a, Transform::from_position(Vec3::ZERO));
world.add(a, Collider::Sphere { radius: 1.0 });
let mut rb_a = RigidBody::dynamic(1.0);
rb_a.gravity_scale = 0.0;
world.add(a, rb_a);
let b = world.spawn();
world.add(b, Transform::from_position(Vec3::new(1.0, 0.0, 0.0)));
world.add(b, Collider::Sphere { radius: 1.0 });
let mut rb_b = RigidBody::dynamic(1.0);
rb_b.gravity_scale = 0.0;
world.add(b, rb_b);
let contacts = detect_collisions(&world);
assert_eq!(contacts.len(), 1);
resolve_collisions(&mut world, &contacts);
let pa = world.get::<Transform>(a).unwrap().position;
let pb = world.get::<Transform>(b).unwrap().position;
let dist = (pb - pa).length();
assert!(dist > 1.0);
}
#[test]
fn test_physics_step_ball_drop() {
let mut world = World::new();
let ball = world.spawn();
world.add(ball, Transform::from_position(Vec3::new(0.0, 5.0, 0.0)));
world.add(ball, Collider::Sphere { radius: 0.5 });
world.add(ball, RigidBody::dynamic(1.0));
let floor = world.spawn();
world.add(floor, Transform::from_position(Vec3::new(0.0, -1.0, 0.0)));
world.add(floor, Collider::Box { half_extents: Vec3::new(10.0, 1.0, 10.0) });
world.add(floor, RigidBody::statik());
let config = PhysicsConfig::default();
for _ in 0..10 {
physics_step(&mut world, &config);
}
let t = world.get::<Transform>(ball).unwrap();
assert!(t.position.y < 5.0);
assert!(t.position.y > -1.0);
}
#[test]
fn test_both_static_no_response() {
let mut world = World::new();
let a = world.spawn();
world.add(a, Transform::from_position(Vec3::ZERO));
world.add(a, Collider::Sphere { radius: 1.0 });
world.add(a, RigidBody::statik());
let b = world.spawn();
world.add(b, Transform::from_position(Vec3::new(0.5, 0.0, 0.0)));
world.add(b, Collider::Sphere { radius: 1.0 });
world.add(b, RigidBody::statik());
let contacts = detect_collisions(&world);
resolve_collisions(&mut world, &contacts);
let pa = world.get::<Transform>(a).unwrap().position;
let pb = world.get::<Transform>(b).unwrap().position;
assert!(approx(pa.x, 0.0));
assert!(approx(pb.x, 0.5));
}
}

View File

@@ -0,0 +1,145 @@
use bytemuck::{Pod, Zeroable};
use crate::hdr::HDR_FORMAT;
/// Number of bloom mip levels (downsample + upsample chain).
pub const BLOOM_MIP_COUNT: usize = 5;
/// Uniform buffer for the bloom pass.
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct BloomUniform {
/// Luminance threshold above which a pixel contributes to bloom.
pub threshold: f32,
/// Soft knee for the threshold.
pub soft_threshold: f32,
pub _padding: [f32; 2],
}
impl Default for BloomUniform {
fn default() -> Self {
Self {
threshold: 1.0,
soft_threshold: 0.5,
_padding: [0.0; 2],
}
}
}
/// GPU resources for the bloom pass (mip chain + uniform buffer).
pub struct BloomResources {
/// One `TextureView` per mip level (5 levels).
pub mip_views: Vec<wgpu::TextureView>,
pub uniform_buffer: wgpu::Buffer,
/// Blend intensity applied during the tonemap pass.
pub intensity: f32,
}
impl BloomResources {
pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
let mip_views = create_mip_views(device, width, height);
let _uniform = BloomUniform::default();
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Bloom Uniform Buffer"),
size: std::mem::size_of::<BloomUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Initialize with defaults via a write at construction time would require a queue;
// callers may write the buffer themselves. The buffer is left zero-initialised here.
Self {
mip_views,
uniform_buffer,
intensity: 0.5,
}
}
/// Recreate the mip-chain textures when the window is resized.
pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
self.mip_views = create_mip_views(device, width, height);
}
}
// ── Public helpers ────────────────────────────────────────────────────────────
/// Return the (width, height) of each bloom mip level.
///
/// Level 0 = width/2 × height/2; each subsequent level halves again.
/// Sizes are clamped to a minimum of 1.
pub fn mip_sizes(width: u32, height: u32) -> Vec<(u32, u32)> {
let mut sizes = Vec::with_capacity(BLOOM_MIP_COUNT);
let mut w = (width / 2).max(1);
let mut h = (height / 2).max(1);
for _ in 0..BLOOM_MIP_COUNT {
sizes.push((w, h));
w = (w / 2).max(1);
h = (h / 2).max(1);
}
sizes
}
// ── Internal helpers ──────────────────────────────────────────────────────────
fn create_mip_views(device: &wgpu::Device, width: u32, height: u32) -> Vec<wgpu::TextureView> {
let sizes = mip_sizes(width, height);
sizes
.into_iter()
.enumerate()
.map(|(i, (w, h))| {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(&format!("Bloom Mip {} Texture", i)),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: HDR_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
texture.create_view(&wgpu::TextureViewDescriptor::default())
})
.collect()
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mip_sizes_1920_1080() {
let sizes = mip_sizes(1920, 1080);
assert_eq!(sizes.len(), BLOOM_MIP_COUNT);
assert_eq!(sizes[0], (960, 540));
assert_eq!(sizes[1], (480, 270));
assert_eq!(sizes[2], (240, 135));
assert_eq!(sizes[3], (120, 67));
assert_eq!(sizes[4], (60, 33));
}
#[test]
fn mip_sizes_64_64() {
let sizes = mip_sizes(64, 64);
assert_eq!(sizes.len(), BLOOM_MIP_COUNT);
assert_eq!(sizes[0], (32, 32));
assert_eq!(sizes[1], (16, 16));
assert_eq!(sizes[2], (8, 8));
assert_eq!(sizes[3], (4, 4));
assert_eq!(sizes[4], (2, 2));
}
#[test]
fn bloom_uniform_default() {
let u = BloomUniform::default();
assert!((u.threshold - 1.0).abs() < f32::EPSILON);
assert!((u.soft_threshold - 0.5).abs() < f32::EPSILON);
assert_eq!(u._padding, [0.0f32; 2]);
}
}

View File

@@ -0,0 +1,112 @@
// Bloom pass shader.
// Two fragment entry points:
// fs_downsample — 5-tap box filter with optional bright extraction
// fs_upsample — 9-tap tent filter for the upsample chain
// ── Group 0 ───────────────────────────────────────────────────────────────────
@group(0) @binding(0) var t_input: texture_2d<f32>;
@group(0) @binding(1) var s_input: sampler;
struct BloomUniform {
threshold: f32,
soft_threshold: f32,
_padding: vec2<f32>,
};
@group(0) @binding(2) var<uniform> bloom: BloomUniform;
// ── Vertex stage ──────────────────────────────────────────────────────────────
struct VertexInput {
@location(0) position: vec2<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs_main(v: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(v.position, 0.0, 1.0);
out.uv = vec2<f32>(v.position.x * 0.5 + 0.5, 1.0 - (v.position.y * 0.5 + 0.5));
return out;
}
// ── Shared helpers ────────────────────────────────────────────────────────────
fn luminance(c: vec3<f32>) -> f32 {
return dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));
}
/// Soft-knee bright-pass: returns the colour with sub-threshold values attenuated.
fn bright_extract(color: vec3<f32>, threshold: f32, soft: f32) -> vec3<f32> {
let lum = luminance(color);
let knee = threshold * soft;
// Quadratic soft knee
let ramp = clamp(lum - threshold + knee, 0.0, 2.0 * knee);
let weight = select(
select(0.0, 1.0, lum >= threshold),
ramp * ramp / (4.0 * knee + 0.00001),
lum >= threshold - knee && lum < threshold,
);
// When lum >= threshold we pass through, otherwise fade via weight
if lum >= threshold {
return color;
}
return color * (weight / max(lum, 0.00001));
}
// ── Downsample pass ───────────────────────────────────────────────────────────
/// 5-sample box filter: centre × 4 + 4 diagonal neighbours × 1, divided by 8.
/// When bloom.threshold > 0 a bright-pass is applied after filtering.
@fragment
fn fs_downsample(in: VertexOutput) -> @location(0) vec4<f32> {
let uv = in.uv;
let texel = 1.0 / vec2<f32>(textureDimensions(t_input));
let center = textureSample(t_input, s_input, uv).rgb;
let tl = textureSample(t_input, s_input, uv + vec2<f32>(-1.0, -1.0) * texel).rgb;
let tr = textureSample(t_input, s_input, uv + vec2<f32>( 1.0, -1.0) * texel).rgb;
let bl = textureSample(t_input, s_input, uv + vec2<f32>(-1.0, 1.0) * texel).rgb;
let br = textureSample(t_input, s_input, uv + vec2<f32>( 1.0, 1.0) * texel).rgb;
var color = (center * 4.0 + tl + tr + bl + br) / 8.0;
// Bright extraction on the first downsample level (threshold guard)
if bloom.threshold > 0.0 {
color = bright_extract(color, bloom.threshold, bloom.soft_threshold);
}
return vec4<f32>(color, 1.0);
}
// ── Upsample pass ─────────────────────────────────────────────────────────────
/// 9-tap tent filter (3×3):
/// corners × 1, axis-aligned edges × 2, centre × 4 → /16
@fragment
fn fs_upsample(in: VertexOutput) -> @location(0) vec4<f32> {
let uv = in.uv;
let texel = 1.0 / vec2<f32>(textureDimensions(t_input));
let tl = textureSample(t_input, s_input, uv + vec2<f32>(-1.0, -1.0) * texel).rgb;
let tc = textureSample(t_input, s_input, uv + vec2<f32>( 0.0, -1.0) * texel).rgb;
let tr = textureSample(t_input, s_input, uv + vec2<f32>( 1.0, -1.0) * texel).rgb;
let ml = textureSample(t_input, s_input, uv + vec2<f32>(-1.0, 0.0) * texel).rgb;
let mc = textureSample(t_input, s_input, uv).rgb;
let mr = textureSample(t_input, s_input, uv + vec2<f32>( 1.0, 0.0) * texel).rgb;
let bl = textureSample(t_input, s_input, uv + vec2<f32>(-1.0, 1.0) * texel).rgb;
let bc = textureSample(t_input, s_input, uv + vec2<f32>( 0.0, 1.0) * texel).rgb;
let br = textureSample(t_input, s_input, uv + vec2<f32>( 1.0, 1.0) * texel).rgb;
// Tent weights: corners×1 + edges×2 + centre×4 → sum=16
let color = (tl + tr + bl + br
+ (tc + ml + mr + bc) * 2.0
+ mc * 4.0) / 16.0;
return vec4<f32>(color, 1.0);
}

View File

@@ -0,0 +1,94 @@
// G-Buffer pass shader for deferred rendering.
// Writes geometry data to multiple render targets.
struct CameraUniform {
view_proj: mat4x4<f32>,
model: mat4x4<f32>,
camera_pos: vec3<f32>,
};
struct MaterialUniform {
base_color: vec4<f32>,
metallic: f32,
roughness: f32,
ao: f32,
};
@group(0) @binding(0) var<uniform> camera: CameraUniform;
@group(1) @binding(0) var t_albedo: texture_2d<f32>;
@group(1) @binding(1) var s_albedo: sampler;
@group(1) @binding(2) var t_normal: texture_2d<f32>;
@group(1) @binding(3) var s_normal: sampler;
@group(2) @binding(0) var<uniform> material: MaterialUniform;
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) uv: vec2<f32>,
@location(3) tangent: vec4<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) world_pos: vec3<f32>,
@location(1) world_normal: vec3<f32>,
@location(2) uv: vec2<f32>,
@location(3) world_tangent: vec3<f32>,
@location(4) world_bitangent: vec3<f32>,
};
@vertex
fn vs_main(v: VertexInput) -> VertexOutput {
var out: VertexOutput;
let world_pos4 = camera.model * vec4<f32>(v.position, 1.0);
out.world_pos = world_pos4.xyz;
out.clip_position = camera.view_proj * world_pos4;
out.uv = v.uv;
let N = normalize((camera.model * vec4<f32>(v.normal, 0.0)).xyz);
let T = normalize((camera.model * vec4<f32>(v.tangent.xyz, 0.0)).xyz);
let B = cross(N, T) * v.tangent.w;
out.world_normal = N;
out.world_tangent = T;
out.world_bitangent = B;
return out;
}
struct GBufferOutput {
@location(0) position: vec4<f32>,
@location(1) normal: vec4<f32>,
@location(2) albedo: vec4<f32>,
@location(3) material_data: vec4<f32>,
};
@fragment
fn fs_main(in: VertexOutput) -> GBufferOutput {
// Sample albedo texture
let tex_color = textureSample(t_albedo, s_albedo, in.uv);
let albedo = material.base_color.rgb * tex_color.rgb;
// Normal mapping via TBN matrix
let T = normalize(in.world_tangent);
let B = normalize(in.world_bitangent);
let N_geom = normalize(in.world_normal);
let normal_sample = textureSample(t_normal, s_normal, in.uv).rgb;
let tangent_normal = normal_sample * 2.0 - 1.0;
// TBN: tangent space → world space
let TBN = mat3x3<f32>(T, B, N_geom);
let N = normalize(TBN * tangent_normal);
var out: GBufferOutput;
out.position = vec4<f32>(in.world_pos, 1.0);
out.normal = vec4<f32>(N * 0.5 + 0.5, 1.0);
out.albedo = vec4<f32>(albedo, material.base_color.a * tex_color.a);
out.material_data = vec4<f32>(material.metallic, material.roughness, material.ao, 1.0);
return out;
}

View File

@@ -0,0 +1,312 @@
// Deferred lighting pass shader.
// Reads G-Buffer textures and computes Cook-Torrance PBR shading.
// ── G-Buffer inputs ──────────────────────────────────────────────────────────
@group(0) @binding(0) var t_position: texture_2d<f32>;
@group(0) @binding(1) var t_normal: texture_2d<f32>;
@group(0) @binding(2) var t_albedo: texture_2d<f32>;
@group(0) @binding(3) var t_material: texture_2d<f32>;
@group(0) @binding(4) var s_gbuffer: sampler;
// ── Lights ───────────────────────────────────────────────────────────────────
struct LightData {
position: vec3<f32>,
light_type: u32,
direction: vec3<f32>,
range: f32,
color: vec3<f32>,
intensity: f32,
inner_cone: f32,
outer_cone: f32,
_padding: vec2<f32>,
};
struct LightsUniform {
lights: array<LightData, 16>,
count: u32,
ambient_color: vec3<f32>,
};
struct CameraPositionUniform {
camera_pos: vec3<f32>,
};
@group(1) @binding(0) var<uniform> lights_uniform: LightsUniform;
@group(1) @binding(1) var<uniform> camera_uniform: CameraPositionUniform;
// ── Shadow + IBL ─────────────────────────────────────────────────────────────
struct ShadowUniform {
light_view_proj: mat4x4<f32>,
shadow_map_size: f32,
shadow_bias: f32,
};
@group(2) @binding(0) var t_shadow: texture_depth_2d;
@group(2) @binding(1) var s_shadow: sampler_comparison;
@group(2) @binding(2) var<uniform> shadow: ShadowUniform;
@group(2) @binding(3) var t_brdf_lut: texture_2d<f32>;
@group(2) @binding(4) var s_brdf_lut: sampler;
@group(2) @binding(5) var t_ssgi: texture_2d<f32>;
@group(2) @binding(6) var s_ssgi: sampler;
@group(2) @binding(7) var t_rt_shadow: texture_2d<f32>;
@group(2) @binding(8) var s_rt_shadow: sampler;
// ── Vertex / Fragment structs ─────────────────────────────────────────────────
struct VertexInput {
@location(0) position: vec2<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs_main(v: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(v.position, 0.0, 1.0);
out.uv = vec2<f32>(v.position.x * 0.5 + 0.5, 1.0 - (v.position.y * 0.5 + 0.5));
return out;
}
// ── BRDF functions (identical to pbr_shader.wgsl) ────────────────────────────
// GGX Normal Distribution Function
fn distribution_ggx(N: vec3<f32>, H: vec3<f32>, roughness: f32) -> f32 {
let a = roughness * roughness;
let a2 = a * a;
let NdotH = max(dot(N, H), 0.0);
let NdotH2 = NdotH * NdotH;
let denom_inner = NdotH2 * (a2 - 1.0) + 1.0;
let denom = 3.14159265358979 * denom_inner * denom_inner;
return a2 / denom;
}
// Schlick-GGX geometry function (single direction)
fn geometry_schlick_ggx(NdotV: f32, roughness: f32) -> f32 {
let r = roughness + 1.0;
let k = (r * r) / 8.0;
return NdotV / (NdotV * (1.0 - k) + k);
}
// Smith geometry function (both directions)
fn geometry_smith(N: vec3<f32>, V: vec3<f32>, L: vec3<f32>, roughness: f32) -> f32 {
let NdotV = max(dot(N, V), 0.0);
let NdotL = max(dot(N, L), 0.0);
let ggx1 = geometry_schlick_ggx(NdotV, roughness);
let ggx2 = geometry_schlick_ggx(NdotL, roughness);
return ggx1 * ggx2;
}
// Fresnel-Schlick approximation
fn fresnel_schlick(cosTheta: f32, F0: vec3<f32>) -> vec3<f32> {
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
// Point light distance attenuation: inverse-square with smooth falloff at range boundary
fn attenuation_point(distance: f32, range: f32) -> f32 {
let d_over_r = distance / range;
let d_over_r4 = d_over_r * d_over_r * d_over_r * d_over_r;
let falloff = clamp(1.0 - d_over_r4, 0.0, 1.0);
return (falloff * falloff) / (distance * distance + 0.0001);
}
// Spot light angular attenuation
fn attenuation_spot(light: LightData, L: vec3<f32>) -> f32 {
let spot_dir = normalize(light.direction);
let theta = dot(spot_dir, -L);
return clamp(
(theta - light.outer_cone) / (light.inner_cone - light.outer_cone + 0.0001),
0.0,
1.0,
);
}
// Cook-Torrance BRDF contribution for one light
fn compute_light_contribution(
light: LightData,
N: vec3<f32>,
V: vec3<f32>,
world_pos: vec3<f32>,
F0: vec3<f32>,
albedo: vec3<f32>,
metallic: f32,
roughness: f32,
) -> vec3<f32> {
var L: vec3<f32>;
var radiance: vec3<f32>;
if light.light_type == 0u {
// Directional
L = normalize(-light.direction);
radiance = light.color * light.intensity;
} else if light.light_type == 1u {
// Point
let to_light = light.position - world_pos;
let dist = length(to_light);
L = normalize(to_light);
let att = attenuation_point(dist, light.range);
radiance = light.color * light.intensity * att;
} else {
// Spot
let to_light = light.position - world_pos;
let dist = length(to_light);
L = normalize(to_light);
let att_dist = attenuation_point(dist, light.range);
let att_ang = attenuation_spot(light, L);
radiance = light.color * light.intensity * att_dist * att_ang;
}
let H = normalize(V + L);
let NDF = distribution_ggx(N, H, roughness);
let G = geometry_smith(N, V, L, roughness);
let F = fresnel_schlick(max(dot(H, V), 0.0), F0);
let ks = F;
let kd = (vec3<f32>(1.0) - ks) * (1.0 - metallic);
let numerator = NDF * G * F;
let NdotL = max(dot(N, L), 0.0);
let NdotV = max(dot(N, V), 0.0);
let denominator = 4.0 * NdotV * NdotL + 0.0001;
let specular = numerator / denominator;
return (kd * albedo / 3.14159265358979 + specular) * radiance * NdotL;
}
fn calculate_shadow(world_pos: vec3<f32>) -> f32 {
// If shadow_map_size == 0, shadow is disabled
if shadow.shadow_map_size == 0.0 {
return 1.0;
}
let light_space_pos = shadow.light_view_proj * vec4<f32>(world_pos, 1.0);
let proj_coords = light_space_pos.xyz / light_space_pos.w;
// wgpu NDC: x,y [-1,1], z [0,1]
let shadow_uv = vec2<f32>(
proj_coords.x * 0.5 + 0.5,
-proj_coords.y * 0.5 + 0.5,
);
let current_depth = proj_coords.z;
if shadow_uv.x < 0.0 || shadow_uv.x > 1.0 || shadow_uv.y < 0.0 || shadow_uv.y > 1.0 {
return 1.0;
}
if current_depth > 1.0 || current_depth < 0.0 {
return 1.0;
}
// 3x3 PCF
let texel_size = 1.0 / shadow.shadow_map_size;
var shadow_val = 0.0;
for (var x = -1; x <= 1; x++) {
for (var y = -1; y <= 1; y++) {
let offset = vec2<f32>(f32(x), f32(y)) * texel_size;
shadow_val += textureSampleCompare(
t_shadow, s_shadow,
shadow_uv + offset,
current_depth - shadow.shadow_bias,
);
}
}
return shadow_val / 9.0;
}
// Procedural environment sampling for IBL
fn sample_environment(direction: vec3<f32>, roughness: f32) -> vec3<f32> {
var env: vec3<f32>;
if direction.y > 0.0 {
let horizon = vec3<f32>(0.6, 0.6, 0.5);
let sky = vec3<f32>(0.3, 0.5, 0.9);
env = mix(horizon, sky, pow(direction.y, 0.4));
} else {
let horizon = vec3<f32>(0.6, 0.6, 0.5);
let ground = vec3<f32>(0.1, 0.08, 0.06);
env = mix(horizon, ground, pow(-direction.y, 0.4));
}
let avg = vec3<f32>(0.3, 0.35, 0.4);
return mix(env, avg, roughness * roughness);
}
// ── Fragment shader ──────────────────────────────────────────────────────────
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let uv = in.uv;
// Read G-Buffer
let position_sample = textureSample(t_position, s_gbuffer, uv);
let world_pos = position_sample.xyz;
// Background pixel: skip shading
if length(world_pos) < 0.001 {
return vec4<f32>(0.01, 0.01, 0.01, 1.0);
}
let normal_sample = textureSample(t_normal, s_gbuffer, uv).rgb;
let N = normalize(normal_sample * 2.0 - 1.0);
let albedo_sample = textureSample(t_albedo, s_gbuffer, uv);
let albedo = albedo_sample.rgb;
let alpha = albedo_sample.a;
let mat_sample = textureSample(t_material, s_gbuffer, uv);
let metallic = mat_sample.r;
let roughness = mat_sample.g;
let ao = mat_sample.b;
let V = normalize(camera_uniform.camera_pos - world_pos);
// F0: base reflectivity; 0.04 for dielectrics, albedo for metals
let F0 = mix(vec3<f32>(0.04, 0.04, 0.04), albedo, metallic);
// Shadow: sample RT shadow texture (1.0 = lit, 0.0 = shadowed)
let rt_dims = textureDimensions(t_rt_shadow);
let rt_coord = vec2<i32>(vec2<f32>(uv.x * f32(rt_dims.x), uv.y * f32(rt_dims.y)));
let shadow_factor = textureLoad(t_rt_shadow, rt_coord, 0).r;
// Accumulate contribution from all active lights
var Lo = vec3<f32>(0.0);
let light_count = min(lights_uniform.count, 16u);
for (var i = 0u; i < light_count; i++) {
var contribution = compute_light_contribution(
lights_uniform.lights[i],
N, V, world_pos, F0, albedo, metallic, roughness,
);
if lights_uniform.lights[i].light_type == 0u {
contribution = contribution * shadow_factor;
}
Lo += contribution;
}
// IBL ambient term
let NdotV_ibl = max(dot(N, V), 0.0);
let R = reflect(-V, N);
// Diffuse IBL
let irradiance = sample_environment(N, 1.0);
let F_env = fresnel_schlick(NdotV_ibl, F0);
let kd_ibl = (vec3<f32>(1.0) - F_env) * (1.0 - metallic);
let diffuse_ibl = kd_ibl * albedo * irradiance;
// Specular IBL
let prefiltered = sample_environment(R, roughness);
let brdf_val = textureSample(t_brdf_lut, s_brdf_lut, vec2<f32>(NdotV_ibl, roughness));
let specular_ibl = prefiltered * (F0 * brdf_val.r + vec3<f32>(brdf_val.g));
let ssgi_data = textureSample(t_ssgi, s_ssgi, uv);
let ssgi_ao = ssgi_data.r;
let ssgi_indirect = ssgi_data.gba;
let ambient = (diffuse_ibl + specular_ibl) * ao * ssgi_ao + ssgi_indirect;
// Output raw HDR linear colour; tonemap is applied in a separate tonemap pass.
let color = ambient + Lo;
return vec4<f32>(color, alpha);
}

View File

@@ -0,0 +1,905 @@
use crate::vertex::MeshVertex;
use crate::fullscreen_quad::FullscreenVertex;
use crate::gpu::DEPTH_FORMAT;
use crate::gbuffer::{
GBUFFER_POSITION_FORMAT, GBUFFER_NORMAL_FORMAT, GBUFFER_ALBEDO_FORMAT, GBUFFER_MATERIAL_FORMAT,
};
use crate::light::CameraUniform;
use crate::ssgi::{SsgiUniform, SSGI_OUTPUT_FORMAT};
use crate::rt_shadow::{RtShadowUniform, RT_SHADOW_FORMAT};
use crate::hdr::HDR_FORMAT;
use crate::bloom::BloomUniform;
use crate::tonemap::TonemapUniform;
/// Bind group layout for the G-Buffer pass camera uniform (dynamic offset, group 0).
pub fn gbuffer_camera_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("GBuffer Camera Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<CameraUniform>() as u64,
),
},
count: None,
},
],
})
}
/// Create the G-Buffer geometry pass render pipeline.
pub fn create_gbuffer_pipeline(
device: &wgpu::Device,
camera_layout: &wgpu::BindGroupLayout,
texture_layout: &wgpu::BindGroupLayout,
material_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("GBuffer Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("deferred_gbuffer.wgsl").into()),
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("GBuffer Pipeline Layout"),
bind_group_layouts: &[camera_layout, texture_layout, material_layout],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("GBuffer Pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[MeshVertex::LAYOUT],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[
Some(wgpu::ColorTargetState {
format: GBUFFER_POSITION_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
Some(wgpu::ColorTargetState {
format: GBUFFER_NORMAL_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
Some(wgpu::ColorTargetState {
format: GBUFFER_ALBEDO_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
Some(wgpu::ColorTargetState {
format: GBUFFER_MATERIAL_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: DEPTH_FORMAT,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
}
/// Bind group layout for the lighting pass G-Buffer textures + sampler (group 0).
/// position is Float non-filterable; normal/albedo/material are filterable.
pub fn lighting_gbuffer_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Lighting GBuffer Bind Group Layout"),
entries: &[
// binding 0: position texture (Rgba32Float → non-filterable)
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 1: normal texture (filterable)
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 2: albedo texture (filterable)
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 3: material texture (filterable)
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 4: non-filtering sampler (required for non-filterable texture)
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
],
})
}
/// Bind group layout for the lighting pass lights + camera position uniforms (group 1).
pub fn lighting_lights_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Lighting Lights Bind Group Layout"),
entries: &[
// binding 0: LightsUniform
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
// binding 1: CameraPositionUniform
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
})
}
/// Bind group layout for shadow map + BRDF LUT (group 2).
pub fn lighting_shadow_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Lighting Shadow Bind Group Layout"),
entries: &[
// binding 0: shadow depth texture
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 1: comparison sampler
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
count: None,
},
// binding 2: ShadowUniform
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
// binding 3: BRDF LUT texture
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 4: filtering sampler for BRDF LUT
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
// binding 5: SSGI output texture (Rgba16Float → filterable)
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 6: filtering sampler for SSGI texture
wgpu::BindGroupLayoutEntry {
binding: 6,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
// binding 7: RT shadow texture (R32Float → non-filterable)
wgpu::BindGroupLayoutEntry {
binding: 7,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 8: non-filtering sampler for RT shadow texture
wgpu::BindGroupLayoutEntry {
binding: 8,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
],
})
}
/// Create the deferred lighting pass render pipeline.
pub fn create_lighting_pipeline(
device: &wgpu::Device,
surface_format: wgpu::TextureFormat,
gbuffer_layout: &wgpu::BindGroupLayout,
lights_layout: &wgpu::BindGroupLayout,
shadow_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Deferred Lighting Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("deferred_lighting.wgsl").into()),
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Deferred Lighting Pipeline Layout"),
bind_group_layouts: &[gbuffer_layout, lights_layout, shadow_layout],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Deferred Lighting Pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[FullscreenVertex::LAYOUT],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
}
// ── SSGI pipeline ─────────────────────────────────────────────────────────────
/// Bind group layout for the SSGI pass G-Buffer inputs (group 0).
/// position is non-filterable (Rgba32Float); normal and albedo are filterable.
/// Sampler is NonFiltering (required when any texture is non-filterable).
pub fn ssgi_gbuffer_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("SSGI GBuffer Bind Group Layout"),
entries: &[
// binding 0: position texture (Rgba32Float → non-filterable)
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 1: normal texture (filterable)
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 2: albedo texture (filterable)
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 3: non-filtering sampler
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
],
})
}
/// Bind group layout for the SSGI pass uniform data (group 1).
/// Contains: SsgiUniform buffer, kernel buffer, noise texture (Rgba32Float), noise sampler.
pub fn ssgi_data_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("SSGI Data Bind Group Layout"),
entries: &[
// binding 0: SsgiUniform
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<SsgiUniform>() as u64,
),
},
count: None,
},
// binding 1: hemisphere kernel (array<vec4<f32>, 64>)
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(
(64 * std::mem::size_of::<[f32; 4]>()) as u64,
),
},
count: None,
},
// binding 2: noise texture (Rgba32Float → non-filterable)
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 3: non-filtering sampler for noise
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
],
})
}
/// Create the SSGI render pipeline.
/// Draws a fullscreen triangle and writes to a single SSGI_OUTPUT_FORMAT color target.
pub fn create_ssgi_pipeline(
device: &wgpu::Device,
gbuffer_layout: &wgpu::BindGroupLayout,
data_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("SSGI Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("ssgi_shader.wgsl").into()),
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("SSGI Pipeline Layout"),
bind_group_layouts: &[gbuffer_layout, data_layout],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("SSGI Pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[FullscreenVertex::LAYOUT],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: SSGI_OUTPUT_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
}
// ── RT Shadow pipeline ────────────────────────────────────────────────────────
/// Bind group layout for the RT shadow compute pass G-Buffer inputs (group 0).
/// Bindings: position (non-filterable), normal (filterable) — both COMPUTE visibility.
pub fn rt_shadow_gbuffer_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("RT Shadow GBuffer Bind Group Layout"),
entries: &[
// binding 0: position texture (Rgba32Float → non-filterable)
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 1: normal texture (filterable)
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
],
})
}
/// Bind group layout for the RT shadow compute pass data (group 1).
/// Bindings: TLAS, storage texture (R32Float write), RtShadowUniform — all COMPUTE visibility.
pub fn rt_shadow_data_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("RT Shadow Data Bind Group Layout"),
entries: &[
// binding 0: TLAS (acceleration structure)
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::AccelerationStructure { vertex_return: false },
count: None,
},
// binding 1: shadow output texture (R32Float, write-only storage)
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::StorageTexture {
access: wgpu::StorageTextureAccess::WriteOnly,
format: RT_SHADOW_FORMAT,
view_dimension: wgpu::TextureViewDimension::D2,
},
count: None,
},
// binding 2: RtShadowUniform
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<RtShadowUniform>() as u64,
),
},
count: None,
},
],
})
}
/// Create the RT shadow compute pipeline.
pub fn create_rt_shadow_pipeline(
device: &wgpu::Device,
gbuffer_layout: &wgpu::BindGroupLayout,
data_layout: &wgpu::BindGroupLayout,
) -> wgpu::ComputePipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("RT Shadow Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("rt_shadow_shader.wgsl").into()),
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("RT Shadow Pipeline Layout"),
bind_group_layouts: &[gbuffer_layout, data_layout],
immediate_size: 0,
});
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("RT Shadow Compute Pipeline"),
layout: Some(&layout),
module: &shader,
entry_point: Some("cs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
cache: None,
})
}
// ── Bloom pipelines ───────────────────────────────────────────────────────────
/// Bind group layout shared by both bloom pipelines (group 0):
/// binding 0 — input texture (filterable)
/// binding 1 — filtering sampler
/// binding 2 — BloomUniform buffer
pub fn bloom_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Bloom Bind Group Layout"),
entries: &[
// binding 0: input HDR texture (filterable)
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 1: filtering sampler
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
// binding 2: BloomUniform buffer
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<BloomUniform>() as u64,
),
},
count: None,
},
],
})
}
/// Create the bloom downsample render pipeline.
/// Entry point: `fs_downsample`. No blending — overwrites the mip target.
pub fn create_bloom_downsample_pipeline(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Bloom Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("bloom_shader.wgsl").into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Bloom Downsample Pipeline Layout"),
bind_group_layouts: &[layout],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Bloom Downsample Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[FullscreenVertex::LAYOUT],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_downsample"),
targets: &[Some(wgpu::ColorTargetState {
format: HDR_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
}
/// Create the bloom upsample render pipeline.
/// Entry point: `fs_upsample`. Additive blending (One + One) to accumulate mips.
pub fn create_bloom_upsample_pipeline(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Bloom Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("bloom_shader.wgsl").into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Bloom Upsample Pipeline Layout"),
bind_group_layouts: &[layout],
immediate_size: 0,
});
let additive = wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
};
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Bloom Upsample Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[FullscreenVertex::LAYOUT],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_upsample"),
targets: &[Some(wgpu::ColorTargetState {
format: HDR_FORMAT,
blend: Some(additive),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
}
// ── Tonemap pipeline ──────────────────────────────────────────────────────────
/// Bind group layout for the tonemap pass (group 0):
/// binding 0 — HDR texture (filterable)
/// binding 1 — bloom texture (filterable)
/// binding 2 — filtering sampler
/// binding 3 — TonemapUniform buffer
pub fn tonemap_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Tonemap Bind Group Layout"),
entries: &[
// binding 0: HDR texture (filterable)
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 1: bloom texture (filterable)
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 2: filtering sampler
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
// binding 3: TonemapUniform buffer
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<TonemapUniform>() as u64,
),
},
count: None,
},
],
})
}
/// Create the tonemap render pipeline.
/// Writes to the surface swapchain format.
pub fn create_tonemap_pipeline(
device: &wgpu::Device,
surface_format: wgpu::TextureFormat,
layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Tonemap Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("tonemap_shader.wgsl").into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Tonemap Pipeline Layout"),
bind_group_layouts: &[layout],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Tonemap Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[FullscreenVertex::LAYOUT],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
}

View File

@@ -0,0 +1,38 @@
use bytemuck::{Pod, Zeroable};
use wgpu::util::DeviceExt;
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct FullscreenVertex {
pub position: [f32; 2],
}
impl FullscreenVertex {
pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<FullscreenVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x2,
},
],
};
}
/// Oversized triangle covering the entire screen (clip-space).
/// Three vertices: (-1,-1), (3,-1), (-1,3)
pub const FULLSCREEN_VERTICES: [FullscreenVertex; 3] = [
FullscreenVertex { position: [-1.0, -1.0] },
FullscreenVertex { position: [ 3.0, -1.0] },
FullscreenVertex { position: [-1.0, 3.0] },
];
pub fn create_fullscreen_vertex_buffer(device: &wgpu::Device) -> wgpu::Buffer {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Fullscreen Triangle Vertex Buffer"),
contents: bytemuck::cast_slice(&FULLSCREEN_VERTICES),
usage: wgpu::BufferUsages::VERTEX,
})
}

View File

@@ -0,0 +1,70 @@
use crate::gpu::DEPTH_FORMAT;
pub const GBUFFER_POSITION_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float;
pub const GBUFFER_NORMAL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
pub const GBUFFER_ALBEDO_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
pub const GBUFFER_MATERIAL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
fn create_rt(
device: &wgpu::Device,
w: u32,
h: u32,
format: wgpu::TextureFormat,
label: &str,
) -> wgpu::TextureView {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
texture.create_view(&wgpu::TextureViewDescriptor::default())
}
fn create_depth(device: &wgpu::Device, w: u32, h: u32) -> wgpu::TextureView {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("GBuffer Depth Texture"),
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
texture.create_view(&wgpu::TextureViewDescriptor::default())
}
pub struct GBuffer {
pub position_view: wgpu::TextureView,
pub normal_view: wgpu::TextureView,
pub albedo_view: wgpu::TextureView,
pub material_view: wgpu::TextureView,
pub depth_view: wgpu::TextureView,
}
impl GBuffer {
pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
let position_view = create_rt(device, width, height, GBUFFER_POSITION_FORMAT, "GBuffer Position");
let normal_view = create_rt(device, width, height, GBUFFER_NORMAL_FORMAT, "GBuffer Normal");
let albedo_view = create_rt(device, width, height, GBUFFER_ALBEDO_FORMAT, "GBuffer Albedo");
let material_view = create_rt(device, width, height, GBUFFER_MATERIAL_FORMAT, "GBuffer Material");
let depth_view = create_depth(device, width, height);
Self {
position_view,
normal_view,
albedo_view,
material_view,
depth_view,
}
}
pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
*self = Self::new(device, width, height);
}
}

View File

@@ -28,10 +28,15 @@ pub struct GpuContext {
impl GpuContext {
pub fn new(window: Arc<Window>) -> Self {
pollster::block_on(Self::new_async(window))
pollster::block_on(Self::new_async(window, wgpu::Features::empty()))
}
async fn new_async(window: Arc<Window>) -> Self {
/// Create a GpuContext requesting additional device features (e.g. ray tracing).
pub fn new_with_features(window: Arc<Window>, features: wgpu::Features) -> Self {
pollster::block_on(Self::new_async(window, features))
}
async fn new_async(window: Arc<Window>, extra_features: wgpu::Features) -> Self {
let size = window.inner_size();
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
@@ -50,12 +55,28 @@ impl GpuContext {
.await
.expect("Failed to find a suitable GPU adapter");
// When extra features are requested (e.g. ray tracing), use adapter limits
// so RT-specific limits (max_acceleration_structures_per_shader_stage, etc.) are satisfied.
let required_limits = if extra_features.is_empty() {
wgpu::Limits::default()
} else {
adapter.limits()
};
let experimental = if extra_features.is_empty() {
wgpu::ExperimentalFeatures::disabled()
} else {
// Safety: we acknowledge experimental features may have bugs
unsafe { wgpu::ExperimentalFeatures::enabled() }
};
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("Voltex Device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
required_features: extra_features,
required_limits,
memory_hints: Default::default(),
experimental_features: experimental,
..Default::default()
})
.await

View File

@@ -0,0 +1,43 @@
/// Texture format used for HDR render targets.
pub const HDR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
/// An HDR render target (Rgba16Float) used as the output of the lighting pass.
pub struct HdrTarget {
pub view: wgpu::TextureView,
pub width: u32,
pub height: u32,
}
impl HdrTarget {
pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
let view = create_hdr_view(device, width, height);
Self { view, width, height }
}
/// Recreate the HDR texture when the window is resized.
pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
self.view = create_hdr_view(device, width, height);
self.width = width;
self.height = height;
}
}
// ── Helpers ───────────────────────────────────────────────────────────────────
fn create_hdr_view(device: &wgpu::Device, width: u32, height: u32) -> wgpu::TextureView {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("HDR Target Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: HDR_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
texture.create_view(&wgpu::TextureViewDescriptor::default())
}

View File

@@ -13,6 +13,15 @@ pub mod shadow;
pub mod shadow_pipeline;
pub mod brdf_lut;
pub mod ibl;
pub mod gbuffer;
pub mod fullscreen_quad;
pub mod deferred_pipeline;
pub mod ssgi;
pub mod rt_accel;
pub mod rt_shadow;
pub mod hdr;
pub mod bloom;
pub mod tonemap;
pub use gpu::{GpuContext, DEPTH_FORMAT};
pub use light::{CameraUniform, LightUniform, LightData, LightsUniform, MAX_LIGHTS, LIGHT_DIRECTIONAL, LIGHT_POINT, LIGHT_SPOT};
@@ -25,3 +34,20 @@ pub use pbr_pipeline::create_pbr_pipeline;
pub use shadow::{ShadowMap, ShadowUniform, ShadowPassUniform, SHADOW_MAP_SIZE, SHADOW_FORMAT};
pub use shadow_pipeline::{create_shadow_pipeline, shadow_pass_bind_group_layout};
pub use ibl::IblResources;
pub use gbuffer::GBuffer;
pub use fullscreen_quad::{create_fullscreen_vertex_buffer, FullscreenVertex};
pub use deferred_pipeline::{
create_gbuffer_pipeline, create_lighting_pipeline,
gbuffer_camera_bind_group_layout,
lighting_gbuffer_bind_group_layout, lighting_lights_bind_group_layout, lighting_shadow_bind_group_layout,
ssgi_gbuffer_bind_group_layout, ssgi_data_bind_group_layout, create_ssgi_pipeline,
rt_shadow_gbuffer_bind_group_layout, rt_shadow_data_bind_group_layout, create_rt_shadow_pipeline,
bloom_bind_group_layout, create_bloom_downsample_pipeline, create_bloom_upsample_pipeline,
tonemap_bind_group_layout, create_tonemap_pipeline,
};
pub use ssgi::{SsgiResources, SsgiUniform, SSGI_OUTPUT_FORMAT};
pub use rt_accel::{RtAccel, RtInstance, BlasMeshData, mat4_to_tlas_transform};
pub use rt_shadow::{RtShadowResources, RtShadowUniform, RT_SHADOW_FORMAT};
pub use hdr::{HdrTarget, HDR_FORMAT};
pub use bloom::{BloomResources, BloomUniform, mip_sizes, BLOOM_MIP_COUNT};
pub use tonemap::{TonemapUniform, aces_tonemap};

View File

@@ -21,4 +21,24 @@ impl Mesh {
});
Self { vertex_buffer, index_buffer, num_indices: indices.len() as u32 }
}
/// Create a mesh with additional buffer usage flags (e.g. `BLAS_INPUT` for ray tracing).
pub fn new_with_usage(
device: &wgpu::Device,
vertices: &[MeshVertex],
indices: &[u32],
extra_usage: wgpu::BufferUsages,
) -> Self {
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Vertex Buffer"),
contents: bytemuck::cast_slice(vertices),
usage: wgpu::BufferUsages::VERTEX | extra_usage,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Index Buffer"),
contents: bytemuck::cast_slice(indices),
usage: wgpu::BufferUsages::INDEX | extra_usage,
});
Self { vertex_buffer, index_buffer, num_indices: indices.len() as u32 }
}
}

View File

@@ -0,0 +1,195 @@
use crate::vertex::MeshVertex;
/// Data needed to build a BLAS for one mesh.
pub struct BlasMeshData<'a> {
pub vertex_buffer: &'a wgpu::Buffer,
pub index_buffer: &'a wgpu::Buffer,
pub vertex_count: u32,
pub index_count: u32,
}
/// One instance transform fed to the TLAS: a world transform and a BLAS index.
pub struct RtInstance {
/// Column-major 4x4 transform matrix.
pub transform: [f32; 16],
/// Index into `RtAccel::blas_list`.
pub blas_index: usize,
}
/// Bottom + Top Level Acceleration Structures for a scene.
pub struct RtAccel {
pub blas_list: Vec<wgpu::Blas>,
pub tlas: wgpu::Tlas,
}
impl RtAccel {
/// Create BLAS for each mesh and build a TLAS with the given instances.
///
/// The encoder must be submitted after this call so the GPU builds fire.
pub fn new(
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
meshes: &[BlasMeshData<'_>],
instances: &[RtInstance],
) -> Self {
let vertex_stride = std::mem::size_of::<MeshVertex>() as u64;
// ── Build one BLAS per mesh ───────────────────────────────────────────
let size_descs: Vec<wgpu::BlasTriangleGeometrySizeDescriptor> = meshes
.iter()
.map(|m| wgpu::BlasTriangleGeometrySizeDescriptor {
vertex_format: wgpu::VertexFormat::Float32x3,
vertex_count: m.vertex_count,
index_format: Some(wgpu::IndexFormat::Uint32),
index_count: Some(m.index_count),
flags: wgpu::AccelerationStructureGeometryFlags::OPAQUE,
})
.collect();
let blas_list: Vec<wgpu::Blas> = meshes
.iter()
.zip(size_descs.iter())
.map(|(_mesh, size_desc)| {
device.create_blas(
&wgpu::CreateBlasDescriptor {
label: Some("Mesh BLAS"),
flags: wgpu::AccelerationStructureFlags::PREFER_FAST_TRACE,
update_mode: wgpu::AccelerationStructureUpdateMode::Build,
},
wgpu::BlasGeometrySizeDescriptors::Triangles {
descriptors: vec![size_desc.clone()],
},
)
})
.collect();
// ── Create TLAS ───────────────────────────────────────────────────────
let max_instances = instances.len().max(1) as u32;
let mut tlas = device.create_tlas(&wgpu::CreateTlasDescriptor {
label: Some("Scene TLAS"),
max_instances,
flags: wgpu::AccelerationStructureFlags::PREFER_FAST_TRACE,
update_mode: wgpu::AccelerationStructureUpdateMode::Build,
});
// ── Populate TLAS instances ───────────────────────────────────────────
for (i, inst) in instances.iter().enumerate() {
let blas = &blas_list[inst.blas_index];
tlas[i] = Some(wgpu::TlasInstance::new(
blas,
mat4_to_tlas_transform(&inst.transform),
0,
0xFF,
));
}
// ── Build entries ─────────────────────────────────────────────────────
let blas_entries: Vec<wgpu::BlasBuildEntry<'_>> = meshes
.iter()
.zip(blas_list.iter())
.zip(size_descs.iter())
.map(|((mesh, blas), size_desc)| wgpu::BlasBuildEntry {
blas,
geometry: wgpu::BlasGeometries::TriangleGeometries(vec![
wgpu::BlasTriangleGeometry {
size: size_desc,
vertex_buffer: mesh.vertex_buffer,
first_vertex: 0,
vertex_stride,
index_buffer: Some(mesh.index_buffer),
first_index: Some(0),
transform_buffer: None,
transform_buffer_offset: None,
},
]),
})
.collect();
encoder.build_acceleration_structures(blas_entries.iter(), std::iter::once(&tlas));
Self { blas_list, tlas }
}
/// Update TLAS instance transforms and rebuild.
pub fn update_instances(
&mut self,
encoder: &mut wgpu::CommandEncoder,
instances: &[RtInstance],
) {
for (i, inst) in instances.iter().enumerate() {
if i >= self.tlas.get().len() {
break;
}
let blas = &self.blas_list[inst.blas_index];
self.tlas[i] = Some(wgpu::TlasInstance::new(
blas,
mat4_to_tlas_transform(&inst.transform),
0,
0xFF,
));
}
encoder.build_acceleration_structures(std::iter::empty(), std::iter::once(&self.tlas));
}
}
/// Convert a column-major 4×4 matrix to a row-major 3×4 affine transform.
///
/// The TLAS expects `[f32; 12]` in row-major order (3 rows × 4 columns).
/// A standard column-major mat4 stores: col0=[m0,m1,m2,m3], col1=[m4,m5,m6,m7], ...
/// Row 0: m[0], m[4], m[8], m[12]
/// Row 1: m[1], m[5], m[9], m[13]
/// Row 2: m[2], m[6], m[10], m[14]
pub fn mat4_to_tlas_transform(m: &[f32; 16]) -> [f32; 12] {
[
m[0], m[4], m[8], m[12],
m[1], m[5], m[9], m[13],
m[2], m[6], m[10], m[14],
]
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mat4_to_tlas_transform_identity() {
#[rustfmt::skip]
let identity: [f32; 16] = [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
];
let result = mat4_to_tlas_transform(&identity);
// Row 0: [1,0,0,0], Row 1: [0,1,0,0], Row 2: [0,0,1,0]
let expected: [f32; 12] = [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
];
assert_eq!(result, expected);
}
#[test]
fn test_mat4_to_tlas_transform_translation() {
// Column-major: identity rotation + translation (tx=1, ty=2, tz=3)
// Column 3 = [tx, ty, tz, 1] stored at indices [12,13,14,15]
#[rustfmt::skip]
let mat: [f32; 16] = [
1.0, 0.0, 0.0, 0.0, // col 0
0.0, 1.0, 0.0, 0.0, // col 1
0.0, 0.0, 1.0, 0.0, // col 2
1.0, 2.0, 3.0, 1.0, // col 3 (translation)
];
let result = mat4_to_tlas_transform(&mat);
// Row-major 3x4: rotation part identity, translation in last column
let expected: [f32; 12] = [
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 2.0,
0.0, 0.0, 1.0, 3.0,
];
assert_eq!(result, expected);
}
}

View File

@@ -0,0 +1,89 @@
use bytemuck::{Pod, Zeroable};
/// Texture format used for the RT shadow output (single-channel float).
pub const RT_SHADOW_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Float;
/// Uniform buffer for the RT shadow compute pass.
///
/// Layout (32 bytes, 16-byte aligned):
/// light_direction [f32; 3] + _pad0: f32 → 16 bytes
/// width: u32, height: u32, _pad1: [u32; 2] → 16 bytes
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct RtShadowUniform {
pub light_direction: [f32; 3],
pub _pad0: f32,
pub width: u32,
pub height: u32,
pub _pad1: [u32; 2],
}
/// GPU resources for the RT shadow compute pass.
pub struct RtShadowResources {
pub shadow_texture: wgpu::Texture,
pub shadow_view: wgpu::TextureView,
pub uniform_buffer: wgpu::Buffer,
pub width: u32,
pub height: u32,
}
impl RtShadowResources {
pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
let (shadow_texture, shadow_view) = create_shadow_texture(device, width, height);
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("RT Shadow Uniform Buffer"),
size: std::mem::size_of::<RtShadowUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Self { shadow_texture, shadow_view, uniform_buffer, width, height }
}
/// Recreate the shadow texture when the window is resized.
pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
let (shadow_texture, shadow_view) = create_shadow_texture(device, width, height);
self.shadow_texture = shadow_texture;
self.shadow_view = shadow_view;
self.width = width;
self.height = height;
}
}
// ── Helpers ──────────────────────────────────────────────────────────────────
fn create_shadow_texture(
device: &wgpu::Device,
width: u32,
height: u32,
) -> (wgpu::Texture, wgpu::TextureView) {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("RT Shadow Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: RT_SHADOW_FORMAT,
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
(texture, view)
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rt_shadow_uniform_size() {
assert_eq!(std::mem::size_of::<RtShadowUniform>(), 32);
}
}

View File

@@ -0,0 +1,78 @@
enable wgpu_ray_query;
// RT Shadow compute shader.
// Reads world-space position and normal from the G-Buffer, then fires a
// shadow ray against the TLAS to determine per-pixel visibility.
// Output is 1.0 (lit) or 0.0 (shadowed) stored in an R32Float texture.
// ── Group 0: G-Buffer inputs ──────────────────────────────────────────────────
@group(0) @binding(0) var t_position: texture_2d<f32>;
@group(0) @binding(1) var t_normal: texture_2d<f32>;
// ── Group 1: RT data ─────────────────────────────────────────────────────────
@group(1) @binding(0) var tlas: acceleration_structure;
@group(1) @binding(1) var t_shadow_out: texture_storage_2d<r32float, write>;
struct RtShadowUniform {
light_direction: vec3<f32>,
_pad0: f32,
width: u32,
height: u32,
_pad1: vec2<u32>,
};
@group(1) @binding(2) var<uniform> uniforms: RtShadowUniform;
// ── Compute entry point ───────────────────────────────────────────────────────
@compute @workgroup_size(8, 8)
fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
let coord = vec2<i32>(i32(gid.x), i32(gid.y));
// Bounds check
if gid.x >= uniforms.width || gid.y >= uniforms.height {
return;
}
// Read world position from G-Buffer
let world_pos = textureLoad(t_position, coord, 0).xyz;
// Background pixel: skip (position is (0,0,0) for skybox pixels)
if dot(world_pos, world_pos) < 0.001 {
textureStore(t_shadow_out, coord, vec4<f32>(1.0, 0.0, 0.0, 0.0));
return;
}
// Read and decode normal — G-Buffer stores N * 0.5 + 0.5
let normal_encoded = textureLoad(t_normal, coord, 0).rgb;
let N = normalize(normal_encoded * 2.0 - 1.0);
// Ray: from surface towards the light, biased along normal to avoid self-intersection
let ray_origin = world_pos + N * 0.01;
let ray_dir = normalize(-uniforms.light_direction);
// Build ray descriptor
var desc: RayDesc;
desc.flags = RAY_FLAG_TERMINATE_ON_FIRST_HIT;
desc.cull_mask = 0xFFu;
desc.tmin = 0.001;
desc.tmax = 1000.0;
desc.origin = ray_origin;
desc.dir = ray_dir;
// Ray query
var rq: ray_query;
rayQueryInitialize(&rq, tlas, desc);
while rayQueryProceed(&rq) {}
// Check result
let intersection = rayQueryGetCommittedIntersection(&rq);
var shadow: f32 = 1.0;
if intersection.kind != RAY_QUERY_INTERSECTION_NONE {
shadow = 0.0;
}
textureStore(t_shadow_out, coord, vec4<f32>(shadow, 0.0, 0.0, 0.0));
}

View File

@@ -0,0 +1,317 @@
use bytemuck::{Pod, Zeroable};
pub const SSGI_OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
pub const SSGI_KERNEL_SIZE: usize = 64;
const NOISE_TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float;
const NOISE_DIM: u32 = 4; // 4x4 = 16 noise samples
/// Uniform buffer for the SSGI pass.
///
/// projection and view are column-major 4x4 matrices stored as flat [f32; 16].
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct SsgiUniform {
pub projection: [f32; 16],
pub view: [f32; 16],
pub radius: f32,
pub bias: f32,
pub intensity: f32,
pub indirect_strength: f32,
}
impl Default for SsgiUniform {
fn default() -> Self {
// Identity matrices for projection and view.
#[rustfmt::skip]
let identity = [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0_f32,
];
Self {
projection: identity,
view: identity,
radius: 0.5,
bias: 0.025,
intensity: 1.5,
indirect_strength: 0.5,
}
}
}
/// GPU resources needed for one SSGI pass.
pub struct SsgiResources {
pub output_view: wgpu::TextureView,
pub kernel_buffer: wgpu::Buffer,
pub noise_view: wgpu::TextureView,
pub noise_sampler: wgpu::Sampler,
pub uniform_buffer: wgpu::Buffer,
pub width: u32,
pub height: u32,
}
impl SsgiResources {
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) -> Self {
let output_view = create_ssgi_output(device, width, height);
// Hemisphere kernel buffer.
let kernel_data = generate_kernel(SSGI_KERNEL_SIZE);
// Flatten [f32; 4] array to bytes.
let kernel_bytes: Vec<u8> = kernel_data
.iter()
.flat_map(|v| {
v.iter()
.flat_map(|f| f.to_ne_bytes())
.collect::<Vec<u8>>()
})
.collect();
let kernel_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("SSGI Kernel Buffer"),
size: kernel_bytes.len() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&kernel_buffer, 0, &kernel_bytes);
// Noise texture.
let (noise_view, noise_sampler) = create_noise_texture(device, queue);
// Uniform buffer with default values.
let uniform = SsgiUniform::default();
let uniform_bytes = bytemuck::bytes_of(&uniform);
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("SSGI Uniform Buffer"),
size: uniform_bytes.len() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&uniform_buffer, 0, uniform_bytes);
Self {
output_view,
kernel_buffer,
noise_view,
noise_sampler,
uniform_buffer,
width,
height,
}
}
/// Recreate only the output texture when the window is resized.
pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
self.output_view = create_ssgi_output(device, width, height);
self.width = width;
self.height = height;
}
}
// ── Helpers ──────────────────────────────────────────────────────────────────
/// Simple deterministic hash → [0, 1) float.
pub fn pseudo_random(seed: u32) -> f32 {
let mut x = seed;
x = x.wrapping_add(0x9e3779b9);
x = x.wrapping_mul(0x6c62272e);
x ^= x >> 16;
x = x.wrapping_mul(0x45d9f3b);
x ^= x >> 16;
(x as f32) / (u32::MAX as f32)
}
/// Generate `count` hemisphere samples (z >= 0) biased towards center.
/// Each sample is stored as [x, y, z, 0.0] (w=0 unused / padding).
pub fn generate_kernel(count: usize) -> Vec<[f32; 4]> {
let mut samples = Vec::with_capacity(count);
let mut seed = 0u32;
for i in 0..count {
// Random point on hemisphere: z in [0, 1], xy distributed on disk.
let r1 = pseudo_random(seed);
seed = seed.wrapping_add(1);
let r2 = pseudo_random(seed);
seed = seed.wrapping_add(1);
// Spherical coordinates: phi in [0, 2π), cos_theta in [0, 1] (upper hemisphere).
let phi = r1 * 2.0 * std::f32::consts::PI;
let cos_theta = r2;
let sin_theta = (1.0 - cos_theta * cos_theta).max(0.0).sqrt();
let x = phi.cos() * sin_theta;
let y = phi.sin() * sin_theta;
let z = cos_theta; // z >= 0 (hemisphere)
// Accelerating interpolation — scale samples closer to origin.
let scale = (i + 1) as f32 / count as f32;
let scale = lerp(0.1, 1.0, scale * scale);
samples.push([x * scale, y * scale, z * scale, 0.0]);
}
samples
}
/// Generate 16 (4×4) random rotation vectors for SSGI noise.
/// Each entry is [cos(angle), sin(angle), 0.0, 0.0] (xy rotation in tangent space).
pub fn generate_noise_data() -> Vec<[f32; 4]> {
let mut data = Vec::with_capacity(16);
let mut seed = 1337u32;
for _ in 0..16 {
let angle = pseudo_random(seed) * 2.0 * std::f32::consts::PI;
seed = seed.wrapping_add(7);
// Normalize: (cos, sin) is already unit length.
data.push([angle.cos(), angle.sin(), 0.0, 0.0]);
}
data
}
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + t * (b - a)
}
/// Create the SSGI output render target (RGBA16Float, screen-sized).
pub fn create_ssgi_output(device: &wgpu::Device, width: u32, height: u32) -> wgpu::TextureView {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("SSGI Output Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: SSGI_OUTPUT_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
texture.create_view(&wgpu::TextureViewDescriptor::default())
}
/// Create the 4×4 SSGI noise texture (Rgba32Float) and its sampler.
pub fn create_noise_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
) -> (wgpu::TextureView, wgpu::Sampler) {
let noise_data = generate_noise_data();
// Flatten [f32; 4] → bytes.
let noise_bytes: Vec<u8> = noise_data
.iter()
.flat_map(|v| v.iter().flat_map(|f| f.to_ne_bytes()).collect::<Vec<u8>>())
.collect();
let extent = wgpu::Extent3d {
width: NOISE_DIM,
height: NOISE_DIM,
depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("SSGI Noise Texture"),
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: NOISE_TEXTURE_FORMAT,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
// Each texel is 4 × 4 bytes = 16 bytes (Rgba32Float).
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&noise_bytes,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(NOISE_DIM * 16), // 4 components × 4 bytes × NOISE_DIM
rows_per_image: Some(NOISE_DIM),
},
extent,
);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
// Wrap/repeat so the noise tiles across the screen.
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("SSGI Noise Sampler"),
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::Repeat,
address_mode_w: wgpu::AddressMode::Repeat,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
(view, sampler)
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kernel_hemisphere() {
let kernel = generate_kernel(SSGI_KERNEL_SIZE);
assert_eq!(kernel.len(), SSGI_KERNEL_SIZE);
for sample in &kernel {
// z component must be >= 0 (upper hemisphere).
assert!(
sample[2] >= 0.0,
"kernel sample z={} is negative",
sample[2]
);
// w component is always 0 (padding).
assert_eq!(sample[3], 0.0);
// Sample must be within the unit sphere (scale <= 1).
let len = (sample[0] * sample[0]
+ sample[1] * sample[1]
+ sample[2] * sample[2])
.sqrt();
assert!(len <= 1.0 + 1e-5, "sample length {} > 1.0", len);
}
}
#[test]
fn test_noise_data() {
let noise = generate_noise_data();
assert_eq!(noise.len(), 16);
for entry in &noise {
// xy should form a unit vector (cos²+sin²=1).
let len = (entry[0] * entry[0] + entry[1] * entry[1]).sqrt();
assert!(
(len - 1.0).abs() < 1e-5,
"noise xy length {} != 1.0",
len
);
// z and w are always 0.
assert_eq!(entry[2], 0.0);
assert_eq!(entry[3], 0.0);
}
}
#[test]
fn test_uniform_defaults() {
let u = SsgiUniform::default();
assert!((u.radius - 0.5).abs() < f32::EPSILON);
assert!((u.bias - 0.025).abs() < f32::EPSILON);
assert!((u.intensity - 1.5).abs() < f32::EPSILON);
assert!((u.indirect_strength - 0.5).abs() < f32::EPSILON);
// Identity diagonal elements.
assert_eq!(u.projection[0], 1.0);
assert_eq!(u.projection[5], 1.0);
assert_eq!(u.projection[10], 1.0);
assert_eq!(u.projection[15], 1.0);
}
}

View File

@@ -0,0 +1,149 @@
// SSGI (Screen-Space Global Illumination) pass shader.
// Reads the G-Buffer and computes per-pixel ambient occlusion + indirect color bleeding.
// Output: vec4(ao, indirect_r, indirect_g, indirect_b)
// ── Group 0: G-Buffer inputs ──────────────────────────────────────────────────
@group(0) @binding(0) var t_position: texture_2d<f32>;
@group(0) @binding(1) var t_normal: texture_2d<f32>;
@group(0) @binding(2) var t_albedo: texture_2d<f32>;
@group(0) @binding(3) var s_gbuffer: sampler;
// ── Group 1: SSGI data ────────────────────────────────────────────────────────
struct SsgiUniform {
projection: mat4x4<f32>,
view: mat4x4<f32>,
radius: f32,
bias: f32,
intensity: f32,
indirect_strength: f32,
};
struct SsgiKernel {
samples: array<vec4<f32>, 64>,
};
@group(1) @binding(0) var<uniform> ssgi: SsgiUniform;
@group(1) @binding(1) var<uniform> kernel: SsgiKernel;
@group(1) @binding(2) var t_noise: texture_2d<f32>;
@group(1) @binding(3) var s_noise: sampler;
// ── Vertex stage ──────────────────────────────────────────────────────────────
struct VertexInput {
@location(0) position: vec2<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs_main(v: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(v.position, 0.0, 1.0);
out.uv = vec2<f32>(v.position.x * 0.5 + 0.5, 1.0 - (v.position.y * 0.5 + 0.5));
return out;
}
// ── Fragment stage ────────────────────────────────────────────────────────────
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let uv = in.uv;
// Read world-space position from G-Buffer.
let world_pos = textureSample(t_position, s_gbuffer, uv).xyz;
// Background pixel: return full AO, no indirect light.
if length(world_pos) < 0.001 {
return vec4<f32>(1.0, 0.0, 0.0, 0.0);
}
// World-space normal (stored as [0,1] encoded, decode back to [-1,1]).
let normal_enc = textureSample(t_normal, s_gbuffer, uv).rgb;
let N_world = normalize(normal_enc * 2.0 - 1.0);
// Albedo for color bleeding.
let albedo = textureSample(t_albedo, s_gbuffer, uv).rgb;
// Convert world-space position and normal to view space.
let view_pos4 = ssgi.view * vec4<f32>(world_pos, 1.0);
let frag_pos_view = view_pos4.xyz;
// Normal matrix = transpose(inverse(view)) ≈ mat3(view) for orthonormal view matrix.
let N_view = normalize((ssgi.view * vec4<f32>(N_world, 0.0)).xyz);
// ── TBN from noise ────────────────────────────────────────────────────────
// Sample a random rotation vector from the noise texture (tiled 4×4 across screen).
let tex_size = vec2<f32>(textureDimensions(t_position));
let noise_uv = uv * tex_size / 4.0; // tile the 4x4 noise over the screen
let rand_vec = textureSample(t_noise, s_noise, noise_uv).xy;
let rand_dir = normalize(vec3<f32>(rand_vec, 0.0));
// Gram-Schmidt orthogonalization to build TBN in view space.
let tangent = normalize(rand_dir - N_view * dot(rand_dir, N_view));
let bitangent = cross(N_view, tangent);
// TBN matrix columns: tangent, bitangent, normal.
// ── 64-sample hemisphere loop ─────────────────────────────────────────────
var occlusion = 0.0;
var indirect_rgb = vec3<f32>(0.0);
for (var i = 0u; i < 64u; i++) {
// Transform kernel sample from tangent space to view space.
let s = kernel.samples[i].xyz;
let sample_vs = tangent * s.x
+ bitangent * s.y
+ N_view * s.z;
// Offset from current view-space fragment position.
let sample_pos = frag_pos_view + sample_vs * ssgi.radius;
// Project sample to get screen UV.
let offset4 = ssgi.projection * vec4<f32>(sample_pos, 1.0);
let offset_ndc = offset4.xyz / offset4.w;
let sample_uv = vec2<f32>(
offset_ndc.x * 0.5 + 0.5,
1.0 - (offset_ndc.y * 0.5 + 0.5),
);
// Clamp to [0,1] to avoid sampling outside the texture.
if sample_uv.x < 0.0 || sample_uv.x > 1.0
|| sample_uv.y < 0.0 || sample_uv.y > 1.0 {
continue;
}
// Actual geometry depth at the projected UV.
let scene_pos_world = textureSample(t_position, s_gbuffer, sample_uv).xyz;
let scene_pos_view = (ssgi.view * vec4<f32>(scene_pos_world, 1.0)).xyz;
let scene_depth = scene_pos_view.z;
// Range check: ignore samples that are too far away (avoid halo artefacts).
let range_check = smoothstep(0.0, 1.0, ssgi.radius / abs(frag_pos_view.z - scene_depth));
// Occlusion: geometry behind the sample blocks light.
let occluded = select(0.0, 1.0, scene_depth >= sample_pos.z + ssgi.bias);
occlusion += occluded * range_check;
// Color bleeding: gather albedo from occluding surfaces.
let neighbor_albedo = textureSample(t_albedo, s_gbuffer, sample_uv).rgb;
indirect_rgb += neighbor_albedo * occluded * range_check;
}
// Normalize by sample count.
let inv_samples = 1.0 / 64.0;
occlusion *= inv_samples;
indirect_rgb *= inv_samples;
// AO: how unoccluded the fragment is (1 = fully lit, 0 = fully occluded).
let ao = 1.0 - occlusion * ssgi.intensity;
let ao_clamped = clamp(ao, 0.0, 1.0);
// Indirect light contribution scaled by indirect_strength and albedo.
let indirect = indirect_rgb * ssgi.indirect_strength * albedo;
return vec4<f32>(ao_clamped, indirect.r, indirect.g, indirect.b);
}

View File

@@ -0,0 +1,71 @@
use bytemuck::{Pod, Zeroable};
/// Uniform buffer for the tonemap pass.
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct TonemapUniform {
/// Bloom contribution weight.
pub bloom_intensity: f32,
/// Pre-tonemap exposure multiplier.
pub exposure: f32,
pub _padding: [f32; 2],
}
impl Default for TonemapUniform {
fn default() -> Self {
Self {
bloom_intensity: 0.5,
exposure: 1.0,
_padding: [0.0; 2],
}
}
}
/// CPU implementation of the ACES filmic tonemap curve (for testing / CPU-side work).
///
/// Formula: clamp((x*(2.51*x+0.03))/(x*(2.43*x+0.59)+0.14), 0, 1)
pub fn aces_tonemap(x: f32) -> f32 {
let num = x * (2.51 * x + 0.03);
let den = x * (2.43 * x + 0.59) + 0.14;
(num / den).clamp(0.0, 1.0)
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aces_zero() {
// aces(0) should be ≈ 0
assert!(aces_tonemap(0.0).abs() < 1e-5, "aces(0) = {}", aces_tonemap(0.0));
}
#[test]
fn aces_one() {
// aces(1) ≈ 0.80 with the standard formula
// clamp((1*(2.51+0.03))/(1*(2.43+0.59)+0.14), 0, 1) = 2.54/3.16 ≈ 0.8038
let v = aces_tonemap(1.0);
assert!(
(v - 0.8038).abs() < 0.001,
"aces(1) = {}, expected ≈ 0.8038",
v
);
}
#[test]
fn aces_large() {
// aces(10) should be very close to 1.0 (saturated)
let v = aces_tonemap(10.0);
assert!(v > 0.999, "aces(10) = {}, expected ≈ 1.0", v);
}
#[test]
fn tonemap_uniform_default() {
let u = TonemapUniform::default();
assert!((u.bloom_intensity - 0.5).abs() < f32::EPSILON);
assert!((u.exposure - 1.0).abs() < f32::EPSILON);
assert_eq!(u._padding, [0.0f32; 2]);
}
}

View File

@@ -0,0 +1,68 @@
// Tonemap pass shader.
// Combines HDR scene colour + bloom, applies exposure and ACES tonemapping,
// then converts to gamma-corrected sRGB for the swapchain.
// ── Group 0 ───────────────────────────────────────────────────────────────────
@group(0) @binding(0) var t_hdr: texture_2d<f32>;
@group(0) @binding(1) var t_bloom: texture_2d<f32>;
@group(0) @binding(2) var s_sampler: sampler;
struct TonemapUniform {
bloom_intensity: f32,
exposure: f32,
_padding: vec2<f32>,
};
@group(0) @binding(3) var<uniform> tonemap: TonemapUniform;
// ── Vertex stage ──────────────────────────────────────────────────────────────
struct VertexInput {
@location(0) position: vec2<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs_main(v: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(v.position, 0.0, 1.0);
out.uv = vec2<f32>(v.position.x * 0.5 + 0.5, 1.0 - (v.position.y * 0.5 + 0.5));
return out;
}
// ── ACES tonemap ──────────────────────────────────────────────────────────────
fn aces_tonemap(x: vec3<f32>) -> vec3<f32> {
let num = x * (2.51 * x + vec3<f32>(0.03));
let den = x * (2.43 * x + vec3<f32>(0.59)) + vec3<f32>(0.14);
return clamp(num / den, vec3<f32>(0.0), vec3<f32>(1.0));
}
// ── Fragment stage ────────────────────────────────────────────────────────────
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let uv = in.uv;
let hdr = textureSample(t_hdr, s_sampler, uv).rgb;
let bloom = textureSample(t_bloom, s_sampler, uv).rgb;
// Combine HDR + bloom
var color = hdr + bloom * tonemap.bloom_intensity;
// Apply exposure
color = color * tonemap.exposure;
// ACES tonemapping
color = aces_tonemap(color);
// Gamma correction (linear → sRGB, γ = 2.2)
color = pow(color, vec3<f32>(1.0 / 2.2));
return vec4<f32>(color, 1.0);
}

View File

@@ -0,0 +1,9 @@
[package]
name = "voltex_script"
version = "0.1.0"
edition = "2021"
[dependencies]
[build-dependencies]
cc = "1"

View File

@@ -0,0 +1,19 @@
fn main() {
let mut build = cc::Build::new();
build.include("lua");
// Add all .c files except lua.c and luac.c (standalone executables with main())
for entry in std::fs::read_dir("lua").unwrap() {
let path = entry.unwrap().path();
if let Some(ext) = path.extension() {
if ext == "c" {
let name = path.file_name().unwrap().to_str().unwrap();
if name != "lua.c" && name != "luac.c" {
build.file(&path);
}
}
}
}
build.compile("lua54");
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
/*
** $Id: lapi.h $
** Auxiliary functions from Lua API
** See Copyright Notice in lua.h
*/
#ifndef lapi_h
#define lapi_h
#include "llimits.h"
#include "lstate.h"
/* Increments 'L->top.p', checking for stack overflows */
#define api_incr_top(L) {L->top.p++; \
api_check(L, L->top.p <= L->ci->top.p, \
"stack overflow");}
/*
** If a call returns too many multiple returns, the callee may not have
** stack space to accommodate all results. In this case, this macro
** increases its stack space ('L->ci->top.p').
*/
#define adjustresults(L,nres) \
{ if ((nres) <= LUA_MULTRET && L->ci->top.p < L->top.p) \
L->ci->top.p = L->top.p; }
/* Ensure the stack has at least 'n' elements */
#define api_checknelems(L,n) \
api_check(L, (n) < (L->top.p - L->ci->func.p), \
"not enough elements in the stack")
/*
** To reduce the overhead of returning from C functions, the presence of
** to-be-closed variables in these functions is coded in the CallInfo's
** field 'nresults', in a way that functions with no to-be-closed variables
** with zero, one, or "all" wanted results have no overhead. Functions
** with other number of wanted results, as well as functions with
** variables to be closed, have an extra check.
*/
#define hastocloseCfunc(n) ((n) < LUA_MULTRET)
/* Map [-1, inf) (range of 'nresults') into (-inf, -2] */
#define codeNresults(n) (-(n) - 3)
#define decodeNresults(n) (-(n) - 3)
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,301 @@
/*
** $Id: lauxlib.h $
** Auxiliary functions for building Lua libraries
** See Copyright Notice in lua.h
*/
#ifndef lauxlib_h
#define lauxlib_h
#include <stddef.h>
#include <stdio.h>
#include "luaconf.h"
#include "lua.h"
/* global table */
#define LUA_GNAME "_G"
typedef struct luaL_Buffer luaL_Buffer;
/* extra error code for 'luaL_loadfilex' */
#define LUA_ERRFILE (LUA_ERRERR+1)
/* key, in the registry, for table of loaded modules */
#define LUA_LOADED_TABLE "_LOADED"
/* key, in the registry, for table of preloaded loaders */
#define LUA_PRELOAD_TABLE "_PRELOAD"
typedef struct luaL_Reg {
const char *name;
lua_CFunction func;
} luaL_Reg;
#define LUAL_NUMSIZES (sizeof(lua_Integer)*16 + sizeof(lua_Number))
LUALIB_API void (luaL_checkversion_) (lua_State *L, lua_Number ver, size_t sz);
#define luaL_checkversion(L) \
luaL_checkversion_(L, LUA_VERSION_NUM, LUAL_NUMSIZES)
LUALIB_API int (luaL_getmetafield) (lua_State *L, int obj, const char *e);
LUALIB_API int (luaL_callmeta) (lua_State *L, int obj, const char *e);
LUALIB_API const char *(luaL_tolstring) (lua_State *L, int idx, size_t *len);
LUALIB_API int (luaL_argerror) (lua_State *L, int arg, const char *extramsg);
LUALIB_API int (luaL_typeerror) (lua_State *L, int arg, const char *tname);
LUALIB_API const char *(luaL_checklstring) (lua_State *L, int arg,
size_t *l);
LUALIB_API const char *(luaL_optlstring) (lua_State *L, int arg,
const char *def, size_t *l);
LUALIB_API lua_Number (luaL_checknumber) (lua_State *L, int arg);
LUALIB_API lua_Number (luaL_optnumber) (lua_State *L, int arg, lua_Number def);
LUALIB_API lua_Integer (luaL_checkinteger) (lua_State *L, int arg);
LUALIB_API lua_Integer (luaL_optinteger) (lua_State *L, int arg,
lua_Integer def);
LUALIB_API void (luaL_checkstack) (lua_State *L, int sz, const char *msg);
LUALIB_API void (luaL_checktype) (lua_State *L, int arg, int t);
LUALIB_API void (luaL_checkany) (lua_State *L, int arg);
LUALIB_API int (luaL_newmetatable) (lua_State *L, const char *tname);
LUALIB_API void (luaL_setmetatable) (lua_State *L, const char *tname);
LUALIB_API void *(luaL_testudata) (lua_State *L, int ud, const char *tname);
LUALIB_API void *(luaL_checkudata) (lua_State *L, int ud, const char *tname);
LUALIB_API void (luaL_where) (lua_State *L, int lvl);
LUALIB_API int (luaL_error) (lua_State *L, const char *fmt, ...);
LUALIB_API int (luaL_checkoption) (lua_State *L, int arg, const char *def,
const char *const lst[]);
LUALIB_API int (luaL_fileresult) (lua_State *L, int stat, const char *fname);
LUALIB_API int (luaL_execresult) (lua_State *L, int stat);
/* predefined references */
#define LUA_NOREF (-2)
#define LUA_REFNIL (-1)
LUALIB_API int (luaL_ref) (lua_State *L, int t);
LUALIB_API void (luaL_unref) (lua_State *L, int t, int ref);
LUALIB_API int (luaL_loadfilex) (lua_State *L, const char *filename,
const char *mode);
#define luaL_loadfile(L,f) luaL_loadfilex(L,f,NULL)
LUALIB_API int (luaL_loadbufferx) (lua_State *L, const char *buff, size_t sz,
const char *name, const char *mode);
LUALIB_API int (luaL_loadstring) (lua_State *L, const char *s);
LUALIB_API lua_State *(luaL_newstate) (void);
LUALIB_API lua_Integer (luaL_len) (lua_State *L, int idx);
LUALIB_API void (luaL_addgsub) (luaL_Buffer *b, const char *s,
const char *p, const char *r);
LUALIB_API const char *(luaL_gsub) (lua_State *L, const char *s,
const char *p, const char *r);
LUALIB_API void (luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup);
LUALIB_API int (luaL_getsubtable) (lua_State *L, int idx, const char *fname);
LUALIB_API void (luaL_traceback) (lua_State *L, lua_State *L1,
const char *msg, int level);
LUALIB_API void (luaL_requiref) (lua_State *L, const char *modname,
lua_CFunction openf, int glb);
/*
** ===============================================================
** some useful macros
** ===============================================================
*/
#define luaL_newlibtable(L,l) \
lua_createtable(L, 0, sizeof(l)/sizeof((l)[0]) - 1)
#define luaL_newlib(L,l) \
(luaL_checkversion(L), luaL_newlibtable(L,l), luaL_setfuncs(L,l,0))
#define luaL_argcheck(L, cond,arg,extramsg) \
((void)(luai_likely(cond) || luaL_argerror(L, (arg), (extramsg))))
#define luaL_argexpected(L,cond,arg,tname) \
((void)(luai_likely(cond) || luaL_typeerror(L, (arg), (tname))))
#define luaL_checkstring(L,n) (luaL_checklstring(L, (n), NULL))
#define luaL_optstring(L,n,d) (luaL_optlstring(L, (n), (d), NULL))
#define luaL_typename(L,i) lua_typename(L, lua_type(L,(i)))
#define luaL_dofile(L, fn) \
(luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))
#define luaL_dostring(L, s) \
(luaL_loadstring(L, s) || lua_pcall(L, 0, LUA_MULTRET, 0))
#define luaL_getmetatable(L,n) (lua_getfield(L, LUA_REGISTRYINDEX, (n)))
#define luaL_opt(L,f,n,d) (lua_isnoneornil(L,(n)) ? (d) : f(L,(n)))
#define luaL_loadbuffer(L,s,sz,n) luaL_loadbufferx(L,s,sz,n,NULL)
/*
** Perform arithmetic operations on lua_Integer values with wrap-around
** semantics, as the Lua core does.
*/
#define luaL_intop(op,v1,v2) \
((lua_Integer)((lua_Unsigned)(v1) op (lua_Unsigned)(v2)))
/* push the value used to represent failure/error */
#define luaL_pushfail(L) lua_pushnil(L)
/*
** Internal assertions for in-house debugging
*/
#if !defined(lua_assert)
#if defined LUAI_ASSERT
#include <assert.h>
#define lua_assert(c) assert(c)
#else
#define lua_assert(c) ((void)0)
#endif
#endif
/*
** {======================================================
** Generic Buffer manipulation
** =======================================================
*/
struct luaL_Buffer {
char *b; /* buffer address */
size_t size; /* buffer size */
size_t n; /* number of characters in buffer */
lua_State *L;
union {
LUAI_MAXALIGN; /* ensure maximum alignment for buffer */
char b[LUAL_BUFFERSIZE]; /* initial buffer */
} init;
};
#define luaL_bufflen(bf) ((bf)->n)
#define luaL_buffaddr(bf) ((bf)->b)
#define luaL_addchar(B,c) \
((void)((B)->n < (B)->size || luaL_prepbuffsize((B), 1)), \
((B)->b[(B)->n++] = (c)))
#define luaL_addsize(B,s) ((B)->n += (s))
#define luaL_buffsub(B,s) ((B)->n -= (s))
LUALIB_API void (luaL_buffinit) (lua_State *L, luaL_Buffer *B);
LUALIB_API char *(luaL_prepbuffsize) (luaL_Buffer *B, size_t sz);
LUALIB_API void (luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l);
LUALIB_API void (luaL_addstring) (luaL_Buffer *B, const char *s);
LUALIB_API void (luaL_addvalue) (luaL_Buffer *B);
LUALIB_API void (luaL_pushresult) (luaL_Buffer *B);
LUALIB_API void (luaL_pushresultsize) (luaL_Buffer *B, size_t sz);
LUALIB_API char *(luaL_buffinitsize) (lua_State *L, luaL_Buffer *B, size_t sz);
#define luaL_prepbuffer(B) luaL_prepbuffsize(B, LUAL_BUFFERSIZE)
/* }====================================================== */
/*
** {======================================================
** File handles for IO library
** =======================================================
*/
/*
** A file handle is a userdata with metatable 'LUA_FILEHANDLE' and
** initial structure 'luaL_Stream' (it may contain other fields
** after that initial structure).
*/
#define LUA_FILEHANDLE "FILE*"
typedef struct luaL_Stream {
FILE *f; /* stream (NULL for incompletely created streams) */
lua_CFunction closef; /* to close stream (NULL for closed streams) */
} luaL_Stream;
/* }====================================================== */
/*
** {==================================================================
** "Abstraction Layer" for basic report of messages and errors
** ===================================================================
*/
/* print a string */
#if !defined(lua_writestring)
#define lua_writestring(s,l) fwrite((s), sizeof(char), (l), stdout)
#endif
/* print a newline and flush the output */
#if !defined(lua_writeline)
#define lua_writeline() (lua_writestring("\n", 1), fflush(stdout))
#endif
/* print an error message */
#if !defined(lua_writestringerror)
#define lua_writestringerror(s,p) \
(fprintf(stderr, (s), (p)), fflush(stderr))
#endif
/* }================================================================== */
/*
** {============================================================
** Compatibility with deprecated conversions
** =============================================================
*/
#if defined(LUA_COMPAT_APIINTCASTS)
#define luaL_checkunsigned(L,a) ((lua_Unsigned)luaL_checkinteger(L,a))
#define luaL_optunsigned(L,a,d) \
((lua_Unsigned)luaL_optinteger(L,a,(lua_Integer)(d)))
#define luaL_checkint(L,n) ((int)luaL_checkinteger(L, (n)))
#define luaL_optint(L,n,d) ((int)luaL_optinteger(L, (n), (d)))
#define luaL_checklong(L,n) ((long)luaL_checkinteger(L, (n)))
#define luaL_optlong(L,n,d) ((long)luaL_optinteger(L, (n), (d)))
#endif
/* }============================================================ */
#endif

View File

@@ -0,0 +1,549 @@
/*
** $Id: lbaselib.c $
** Basic library
** See Copyright Notice in lua.h
*/
#define lbaselib_c
#define LUA_LIB
#include "lprefix.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
static int luaB_print (lua_State *L) {
int n = lua_gettop(L); /* number of arguments */
int i;
for (i = 1; i <= n; i++) { /* for each argument */
size_t l;
const char *s = luaL_tolstring(L, i, &l); /* convert it to string */
if (i > 1) /* not the first element? */
lua_writestring("\t", 1); /* add a tab before it */
lua_writestring(s, l); /* print it */
lua_pop(L, 1); /* pop result */
}
lua_writeline();
return 0;
}
/*
** Creates a warning with all given arguments.
** Check first for errors; otherwise an error may interrupt
** the composition of a warning, leaving it unfinished.
*/
static int luaB_warn (lua_State *L) {
int n = lua_gettop(L); /* number of arguments */
int i;
luaL_checkstring(L, 1); /* at least one argument */
for (i = 2; i <= n; i++)
luaL_checkstring(L, i); /* make sure all arguments are strings */
for (i = 1; i < n; i++) /* compose warning */
lua_warning(L, lua_tostring(L, i), 1);
lua_warning(L, lua_tostring(L, n), 0); /* close warning */
return 0;
}
#define SPACECHARS " \f\n\r\t\v"
static const char *b_str2int (const char *s, int base, lua_Integer *pn) {
lua_Unsigned n = 0;
int neg = 0;
s += strspn(s, SPACECHARS); /* skip initial spaces */
if (*s == '-') { s++; neg = 1; } /* handle sign */
else if (*s == '+') s++;
if (!isalnum((unsigned char)*s)) /* no digit? */
return NULL;
do {
int digit = (isdigit((unsigned char)*s)) ? *s - '0'
: (toupper((unsigned char)*s) - 'A') + 10;
if (digit >= base) return NULL; /* invalid numeral */
n = n * base + digit;
s++;
} while (isalnum((unsigned char)*s));
s += strspn(s, SPACECHARS); /* skip trailing spaces */
*pn = (lua_Integer)((neg) ? (0u - n) : n);
return s;
}
static int luaB_tonumber (lua_State *L) {
if (lua_isnoneornil(L, 2)) { /* standard conversion? */
if (lua_type(L, 1) == LUA_TNUMBER) { /* already a number? */
lua_settop(L, 1); /* yes; return it */
return 1;
}
else {
size_t l;
const char *s = lua_tolstring(L, 1, &l);
if (s != NULL && lua_stringtonumber(L, s) == l + 1)
return 1; /* successful conversion to number */
/* else not a number */
luaL_checkany(L, 1); /* (but there must be some parameter) */
}
}
else {
size_t l;
const char *s;
lua_Integer n = 0; /* to avoid warnings */
lua_Integer base = luaL_checkinteger(L, 2);
luaL_checktype(L, 1, LUA_TSTRING); /* no numbers as strings */
s = lua_tolstring(L, 1, &l);
luaL_argcheck(L, 2 <= base && base <= 36, 2, "base out of range");
if (b_str2int(s, (int)base, &n) == s + l) {
lua_pushinteger(L, n);
return 1;
} /* else not a number */
} /* else not a number */
luaL_pushfail(L); /* not a number */
return 1;
}
static int luaB_error (lua_State *L) {
int level = (int)luaL_optinteger(L, 2, 1);
lua_settop(L, 1);
if (lua_type(L, 1) == LUA_TSTRING && level > 0) {
luaL_where(L, level); /* add extra information */
lua_pushvalue(L, 1);
lua_concat(L, 2);
}
return lua_error(L);
}
static int luaB_getmetatable (lua_State *L) {
luaL_checkany(L, 1);
if (!lua_getmetatable(L, 1)) {
lua_pushnil(L);
return 1; /* no metatable */
}
luaL_getmetafield(L, 1, "__metatable");
return 1; /* returns either __metatable field (if present) or metatable */
}
static int luaB_setmetatable (lua_State *L) {
int t = lua_type(L, 2);
luaL_checktype(L, 1, LUA_TTABLE);
luaL_argexpected(L, t == LUA_TNIL || t == LUA_TTABLE, 2, "nil or table");
if (l_unlikely(luaL_getmetafield(L, 1, "__metatable") != LUA_TNIL))
return luaL_error(L, "cannot change a protected metatable");
lua_settop(L, 2);
lua_setmetatable(L, 1);
return 1;
}
static int luaB_rawequal (lua_State *L) {
luaL_checkany(L, 1);
luaL_checkany(L, 2);
lua_pushboolean(L, lua_rawequal(L, 1, 2));
return 1;
}
static int luaB_rawlen (lua_State *L) {
int t = lua_type(L, 1);
luaL_argexpected(L, t == LUA_TTABLE || t == LUA_TSTRING, 1,
"table or string");
lua_pushinteger(L, lua_rawlen(L, 1));
return 1;
}
static int luaB_rawget (lua_State *L) {
luaL_checktype(L, 1, LUA_TTABLE);
luaL_checkany(L, 2);
lua_settop(L, 2);
lua_rawget(L, 1);
return 1;
}
static int luaB_rawset (lua_State *L) {
luaL_checktype(L, 1, LUA_TTABLE);
luaL_checkany(L, 2);
luaL_checkany(L, 3);
lua_settop(L, 3);
lua_rawset(L, 1);
return 1;
}
static int pushmode (lua_State *L, int oldmode) {
if (oldmode == -1)
luaL_pushfail(L); /* invalid call to 'lua_gc' */
else
lua_pushstring(L, (oldmode == LUA_GCINC) ? "incremental"
: "generational");
return 1;
}
/*
** check whether call to 'lua_gc' was valid (not inside a finalizer)
*/
#define checkvalres(res) { if (res == -1) break; }
static int luaB_collectgarbage (lua_State *L) {
static const char *const opts[] = {"stop", "restart", "collect",
"count", "step", "setpause", "setstepmul",
"isrunning", "generational", "incremental", NULL};
static const int optsnum[] = {LUA_GCSTOP, LUA_GCRESTART, LUA_GCCOLLECT,
LUA_GCCOUNT, LUA_GCSTEP, LUA_GCSETPAUSE, LUA_GCSETSTEPMUL,
LUA_GCISRUNNING, LUA_GCGEN, LUA_GCINC};
int o = optsnum[luaL_checkoption(L, 1, "collect", opts)];
switch (o) {
case LUA_GCCOUNT: {
int k = lua_gc(L, o);
int b = lua_gc(L, LUA_GCCOUNTB);
checkvalres(k);
lua_pushnumber(L, (lua_Number)k + ((lua_Number)b/1024));
return 1;
}
case LUA_GCSTEP: {
int step = (int)luaL_optinteger(L, 2, 0);
int res = lua_gc(L, o, step);
checkvalres(res);
lua_pushboolean(L, res);
return 1;
}
case LUA_GCSETPAUSE:
case LUA_GCSETSTEPMUL: {
int p = (int)luaL_optinteger(L, 2, 0);
int previous = lua_gc(L, o, p);
checkvalres(previous);
lua_pushinteger(L, previous);
return 1;
}
case LUA_GCISRUNNING: {
int res = lua_gc(L, o);
checkvalres(res);
lua_pushboolean(L, res);
return 1;
}
case LUA_GCGEN: {
int minormul = (int)luaL_optinteger(L, 2, 0);
int majormul = (int)luaL_optinteger(L, 3, 0);
return pushmode(L, lua_gc(L, o, minormul, majormul));
}
case LUA_GCINC: {
int pause = (int)luaL_optinteger(L, 2, 0);
int stepmul = (int)luaL_optinteger(L, 3, 0);
int stepsize = (int)luaL_optinteger(L, 4, 0);
return pushmode(L, lua_gc(L, o, pause, stepmul, stepsize));
}
default: {
int res = lua_gc(L, o);
checkvalres(res);
lua_pushinteger(L, res);
return 1;
}
}
luaL_pushfail(L); /* invalid call (inside a finalizer) */
return 1;
}
static int luaB_type (lua_State *L) {
int t = lua_type(L, 1);
luaL_argcheck(L, t != LUA_TNONE, 1, "value expected");
lua_pushstring(L, lua_typename(L, t));
return 1;
}
static int luaB_next (lua_State *L) {
luaL_checktype(L, 1, LUA_TTABLE);
lua_settop(L, 2); /* create a 2nd argument if there isn't one */
if (lua_next(L, 1))
return 2;
else {
lua_pushnil(L);
return 1;
}
}
static int pairscont (lua_State *L, int status, lua_KContext k) {
(void)L; (void)status; (void)k; /* unused */
return 3;
}
static int luaB_pairs (lua_State *L) {
luaL_checkany(L, 1);
if (luaL_getmetafield(L, 1, "__pairs") == LUA_TNIL) { /* no metamethod? */
lua_pushcfunction(L, luaB_next); /* will return generator, */
lua_pushvalue(L, 1); /* state, */
lua_pushnil(L); /* and initial value */
}
else {
lua_pushvalue(L, 1); /* argument 'self' to metamethod */
lua_callk(L, 1, 3, 0, pairscont); /* get 3 values from metamethod */
}
return 3;
}
/*
** Traversal function for 'ipairs'
*/
static int ipairsaux (lua_State *L) {
lua_Integer i = luaL_checkinteger(L, 2);
i = luaL_intop(+, i, 1);
lua_pushinteger(L, i);
return (lua_geti(L, 1, i) == LUA_TNIL) ? 1 : 2;
}
/*
** 'ipairs' function. Returns 'ipairsaux', given "table", 0.
** (The given "table" may not be a table.)
*/
static int luaB_ipairs (lua_State *L) {
luaL_checkany(L, 1);
lua_pushcfunction(L, ipairsaux); /* iteration function */
lua_pushvalue(L, 1); /* state */
lua_pushinteger(L, 0); /* initial value */
return 3;
}
static int load_aux (lua_State *L, int status, int envidx) {
if (l_likely(status == LUA_OK)) {
if (envidx != 0) { /* 'env' parameter? */
lua_pushvalue(L, envidx); /* environment for loaded function */
if (!lua_setupvalue(L, -2, 1)) /* set it as 1st upvalue */
lua_pop(L, 1); /* remove 'env' if not used by previous call */
}
return 1;
}
else { /* error (message is on top of the stack) */
luaL_pushfail(L);
lua_insert(L, -2); /* put before error message */
return 2; /* return fail plus error message */
}
}
static int luaB_loadfile (lua_State *L) {
const char *fname = luaL_optstring(L, 1, NULL);
const char *mode = luaL_optstring(L, 2, NULL);
int env = (!lua_isnone(L, 3) ? 3 : 0); /* 'env' index or 0 if no 'env' */
int status = luaL_loadfilex(L, fname, mode);
return load_aux(L, status, env);
}
/*
** {======================================================
** Generic Read function
** =======================================================
*/
/*
** reserved slot, above all arguments, to hold a copy of the returned
** string to avoid it being collected while parsed. 'load' has four
** optional arguments (chunk, source name, mode, and environment).
*/
#define RESERVEDSLOT 5
/*
** Reader for generic 'load' function: 'lua_load' uses the
** stack for internal stuff, so the reader cannot change the
** stack top. Instead, it keeps its resulting string in a
** reserved slot inside the stack.
*/
static const char *generic_reader (lua_State *L, void *ud, size_t *size) {
(void)(ud); /* not used */
luaL_checkstack(L, 2, "too many nested functions");
lua_pushvalue(L, 1); /* get function */
lua_call(L, 0, 1); /* call it */
if (lua_isnil(L, -1)) {
lua_pop(L, 1); /* pop result */
*size = 0;
return NULL;
}
else if (l_unlikely(!lua_isstring(L, -1)))
luaL_error(L, "reader function must return a string");
lua_replace(L, RESERVEDSLOT); /* save string in reserved slot */
return lua_tolstring(L, RESERVEDSLOT, size);
}
static int luaB_load (lua_State *L) {
int status;
size_t l;
const char *s = lua_tolstring(L, 1, &l);
const char *mode = luaL_optstring(L, 3, "bt");
int env = (!lua_isnone(L, 4) ? 4 : 0); /* 'env' index or 0 if no 'env' */
if (s != NULL) { /* loading a string? */
const char *chunkname = luaL_optstring(L, 2, s);
status = luaL_loadbufferx(L, s, l, chunkname, mode);
}
else { /* loading from a reader function */
const char *chunkname = luaL_optstring(L, 2, "=(load)");
luaL_checktype(L, 1, LUA_TFUNCTION);
lua_settop(L, RESERVEDSLOT); /* create reserved slot */
status = lua_load(L, generic_reader, NULL, chunkname, mode);
}
return load_aux(L, status, env);
}
/* }====================================================== */
static int dofilecont (lua_State *L, int d1, lua_KContext d2) {
(void)d1; (void)d2; /* only to match 'lua_Kfunction' prototype */
return lua_gettop(L) - 1;
}
static int luaB_dofile (lua_State *L) {
const char *fname = luaL_optstring(L, 1, NULL);
lua_settop(L, 1);
if (l_unlikely(luaL_loadfile(L, fname) != LUA_OK))
return lua_error(L);
lua_callk(L, 0, LUA_MULTRET, 0, dofilecont);
return dofilecont(L, 0, 0);
}
static int luaB_assert (lua_State *L) {
if (l_likely(lua_toboolean(L, 1))) /* condition is true? */
return lua_gettop(L); /* return all arguments */
else { /* error */
luaL_checkany(L, 1); /* there must be a condition */
lua_remove(L, 1); /* remove it */
lua_pushliteral(L, "assertion failed!"); /* default message */
lua_settop(L, 1); /* leave only message (default if no other one) */
return luaB_error(L); /* call 'error' */
}
}
static int luaB_select (lua_State *L) {
int n = lua_gettop(L);
if (lua_type(L, 1) == LUA_TSTRING && *lua_tostring(L, 1) == '#') {
lua_pushinteger(L, n-1);
return 1;
}
else {
lua_Integer i = luaL_checkinteger(L, 1);
if (i < 0) i = n + i;
else if (i > n) i = n;
luaL_argcheck(L, 1 <= i, 1, "index out of range");
return n - (int)i;
}
}
/*
** Continuation function for 'pcall' and 'xpcall'. Both functions
** already pushed a 'true' before doing the call, so in case of success
** 'finishpcall' only has to return everything in the stack minus
** 'extra' values (where 'extra' is exactly the number of items to be
** ignored).
*/
static int finishpcall (lua_State *L, int status, lua_KContext extra) {
if (l_unlikely(status != LUA_OK && status != LUA_YIELD)) { /* error? */
lua_pushboolean(L, 0); /* first result (false) */
lua_pushvalue(L, -2); /* error message */
return 2; /* return false, msg */
}
else
return lua_gettop(L) - (int)extra; /* return all results */
}
static int luaB_pcall (lua_State *L) {
int status;
luaL_checkany(L, 1);
lua_pushboolean(L, 1); /* first result if no errors */
lua_insert(L, 1); /* put it in place */
status = lua_pcallk(L, lua_gettop(L) - 2, LUA_MULTRET, 0, 0, finishpcall);
return finishpcall(L, status, 0);
}
/*
** Do a protected call with error handling. After 'lua_rotate', the
** stack will have <f, err, true, f, [args...]>; so, the function passes
** 2 to 'finishpcall' to skip the 2 first values when returning results.
*/
static int luaB_xpcall (lua_State *L) {
int status;
int n = lua_gettop(L);
luaL_checktype(L, 2, LUA_TFUNCTION); /* check error function */
lua_pushboolean(L, 1); /* first result */
lua_pushvalue(L, 1); /* function */
lua_rotate(L, 3, 2); /* move them below function's arguments */
status = lua_pcallk(L, n - 2, LUA_MULTRET, 2, 2, finishpcall);
return finishpcall(L, status, 2);
}
static int luaB_tostring (lua_State *L) {
luaL_checkany(L, 1);
luaL_tolstring(L, 1, NULL);
return 1;
}
static const luaL_Reg base_funcs[] = {
{"assert", luaB_assert},
{"collectgarbage", luaB_collectgarbage},
{"dofile", luaB_dofile},
{"error", luaB_error},
{"getmetatable", luaB_getmetatable},
{"ipairs", luaB_ipairs},
{"loadfile", luaB_loadfile},
{"load", luaB_load},
{"next", luaB_next},
{"pairs", luaB_pairs},
{"pcall", luaB_pcall},
{"print", luaB_print},
{"warn", luaB_warn},
{"rawequal", luaB_rawequal},
{"rawlen", luaB_rawlen},
{"rawget", luaB_rawget},
{"rawset", luaB_rawset},
{"select", luaB_select},
{"setmetatable", luaB_setmetatable},
{"tonumber", luaB_tonumber},
{"tostring", luaB_tostring},
{"type", luaB_type},
{"xpcall", luaB_xpcall},
/* placeholders */
{LUA_GNAME, NULL},
{"_VERSION", NULL},
{NULL, NULL}
};
LUAMOD_API int luaopen_base (lua_State *L) {
/* open lib into global table */
lua_pushglobaltable(L);
luaL_setfuncs(L, base_funcs, 0);
/* set global _G */
lua_pushvalue(L, -1);
lua_setfield(L, -2, LUA_GNAME);
/* set global _VERSION */
lua_pushliteral(L, LUA_VERSION);
lua_setfield(L, -2, "_VERSION");
return 1;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,101 @@
/*
** $Id: lcode.h $
** Code generator for Lua
** See Copyright Notice in lua.h
*/
#ifndef lcode_h
#define lcode_h
#include "llex.h"
#include "lobject.h"
#include "lopcodes.h"
#include "lparser.h"
/*
** Marks the end of a patch list. It is an invalid value both as an absolute
** address, and as a list link (would link an element to itself).
*/
#define NO_JUMP (-1)
/*
** grep "ORDER OPR" if you change these enums (ORDER OP)
*/
typedef enum BinOpr {
/* arithmetic operators */
OPR_ADD, OPR_SUB, OPR_MUL, OPR_MOD, OPR_POW,
OPR_DIV, OPR_IDIV,
/* bitwise operators */
OPR_BAND, OPR_BOR, OPR_BXOR,
OPR_SHL, OPR_SHR,
/* string operator */
OPR_CONCAT,
/* comparison operators */
OPR_EQ, OPR_LT, OPR_LE,
OPR_NE, OPR_GT, OPR_GE,
/* logical operators */
OPR_AND, OPR_OR,
OPR_NOBINOPR
} BinOpr;
/* true if operation is foldable (that is, it is arithmetic or bitwise) */
#define foldbinop(op) ((op) <= OPR_SHR)
#define luaK_codeABC(fs,o,a,b,c) luaK_codeABCk(fs,o,a,b,c,0)
typedef enum UnOpr { OPR_MINUS, OPR_BNOT, OPR_NOT, OPR_LEN, OPR_NOUNOPR } UnOpr;
/* get (pointer to) instruction of given 'expdesc' */
#define getinstruction(fs,e) ((fs)->f->code[(e)->u.info])
#define luaK_setmultret(fs,e) luaK_setreturns(fs, e, LUA_MULTRET)
#define luaK_jumpto(fs,t) luaK_patchlist(fs, luaK_jump(fs), t)
LUAI_FUNC int luaK_code (FuncState *fs, Instruction i);
LUAI_FUNC int luaK_codeABx (FuncState *fs, OpCode o, int A, unsigned int Bx);
LUAI_FUNC int luaK_codeABCk (FuncState *fs, OpCode o, int A,
int B, int C, int k);
LUAI_FUNC int luaK_exp2const (FuncState *fs, const expdesc *e, TValue *v);
LUAI_FUNC void luaK_fixline (FuncState *fs, int line);
LUAI_FUNC void luaK_nil (FuncState *fs, int from, int n);
LUAI_FUNC void luaK_reserveregs (FuncState *fs, int n);
LUAI_FUNC void luaK_checkstack (FuncState *fs, int n);
LUAI_FUNC void luaK_int (FuncState *fs, int reg, lua_Integer n);
LUAI_FUNC void luaK_dischargevars (FuncState *fs, expdesc *e);
LUAI_FUNC int luaK_exp2anyreg (FuncState *fs, expdesc *e);
LUAI_FUNC void luaK_exp2anyregup (FuncState *fs, expdesc *e);
LUAI_FUNC void luaK_exp2nextreg (FuncState *fs, expdesc *e);
LUAI_FUNC void luaK_exp2val (FuncState *fs, expdesc *e);
LUAI_FUNC void luaK_self (FuncState *fs, expdesc *e, expdesc *key);
LUAI_FUNC void luaK_indexed (FuncState *fs, expdesc *t, expdesc *k);
LUAI_FUNC void luaK_goiftrue (FuncState *fs, expdesc *e);
LUAI_FUNC void luaK_goiffalse (FuncState *fs, expdesc *e);
LUAI_FUNC void luaK_storevar (FuncState *fs, expdesc *var, expdesc *e);
LUAI_FUNC void luaK_setreturns (FuncState *fs, expdesc *e, int nresults);
LUAI_FUNC void luaK_setoneret (FuncState *fs, expdesc *e);
LUAI_FUNC int luaK_jump (FuncState *fs);
LUAI_FUNC void luaK_ret (FuncState *fs, int first, int nret);
LUAI_FUNC void luaK_patchlist (FuncState *fs, int list, int target);
LUAI_FUNC void luaK_patchtohere (FuncState *fs, int list);
LUAI_FUNC void luaK_concat (FuncState *fs, int *l1, int l2);
LUAI_FUNC int luaK_getlabel (FuncState *fs);
LUAI_FUNC void luaK_prefix (FuncState *fs, UnOpr op, expdesc *v, int line);
LUAI_FUNC void luaK_infix (FuncState *fs, BinOpr op, expdesc *v);
LUAI_FUNC void luaK_posfix (FuncState *fs, BinOpr op, expdesc *v1,
expdesc *v2, int line);
LUAI_FUNC void luaK_settablesize (FuncState *fs, int pc,
int ra, int asize, int hsize);
LUAI_FUNC void luaK_setlist (FuncState *fs, int base, int nelems, int tostore);
LUAI_FUNC void luaK_finish (FuncState *fs);
LUAI_FUNC l_noret luaK_semerror (LexState *ls, const char *msg);
#endif

View File

@@ -0,0 +1,210 @@
/*
** $Id: lcorolib.c $
** Coroutine Library
** See Copyright Notice in lua.h
*/
#define lcorolib_c
#define LUA_LIB
#include "lprefix.h"
#include <stdlib.h>
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
static lua_State *getco (lua_State *L) {
lua_State *co = lua_tothread(L, 1);
luaL_argexpected(L, co, 1, "thread");
return co;
}
/*
** Resumes a coroutine. Returns the number of results for non-error
** cases or -1 for errors.
*/
static int auxresume (lua_State *L, lua_State *co, int narg) {
int status, nres;
if (l_unlikely(!lua_checkstack(co, narg))) {
lua_pushliteral(L, "too many arguments to resume");
return -1; /* error flag */
}
lua_xmove(L, co, narg);
status = lua_resume(co, L, narg, &nres);
if (l_likely(status == LUA_OK || status == LUA_YIELD)) {
if (l_unlikely(!lua_checkstack(L, nres + 1))) {
lua_pop(co, nres); /* remove results anyway */
lua_pushliteral(L, "too many results to resume");
return -1; /* error flag */
}
lua_xmove(co, L, nres); /* move yielded values */
return nres;
}
else {
lua_xmove(co, L, 1); /* move error message */
return -1; /* error flag */
}
}
static int luaB_coresume (lua_State *L) {
lua_State *co = getco(L);
int r;
r = auxresume(L, co, lua_gettop(L) - 1);
if (l_unlikely(r < 0)) {
lua_pushboolean(L, 0);
lua_insert(L, -2);
return 2; /* return false + error message */
}
else {
lua_pushboolean(L, 1);
lua_insert(L, -(r + 1));
return r + 1; /* return true + 'resume' returns */
}
}
static int luaB_auxwrap (lua_State *L) {
lua_State *co = lua_tothread(L, lua_upvalueindex(1));
int r = auxresume(L, co, lua_gettop(L));
if (l_unlikely(r < 0)) { /* error? */
int stat = lua_status(co);
if (stat != LUA_OK && stat != LUA_YIELD) { /* error in the coroutine? */
stat = lua_closethread(co, L); /* close its tbc variables */
lua_assert(stat != LUA_OK);
lua_xmove(co, L, 1); /* move error message to the caller */
}
if (stat != LUA_ERRMEM && /* not a memory error and ... */
lua_type(L, -1) == LUA_TSTRING) { /* ... error object is a string? */
luaL_where(L, 1); /* add extra info, if available */
lua_insert(L, -2);
lua_concat(L, 2);
}
return lua_error(L); /* propagate error */
}
return r;
}
static int luaB_cocreate (lua_State *L) {
lua_State *NL;
luaL_checktype(L, 1, LUA_TFUNCTION);
NL = lua_newthread(L);
lua_pushvalue(L, 1); /* move function to top */
lua_xmove(L, NL, 1); /* move function from L to NL */
return 1;
}
static int luaB_cowrap (lua_State *L) {
luaB_cocreate(L);
lua_pushcclosure(L, luaB_auxwrap, 1);
return 1;
}
static int luaB_yield (lua_State *L) {
return lua_yield(L, lua_gettop(L));
}
#define COS_RUN 0
#define COS_DEAD 1
#define COS_YIELD 2
#define COS_NORM 3
static const char *const statname[] =
{"running", "dead", "suspended", "normal"};
static int auxstatus (lua_State *L, lua_State *co) {
if (L == co) return COS_RUN;
else {
switch (lua_status(co)) {
case LUA_YIELD:
return COS_YIELD;
case LUA_OK: {
lua_Debug ar;
if (lua_getstack(co, 0, &ar)) /* does it have frames? */
return COS_NORM; /* it is running */
else if (lua_gettop(co) == 0)
return COS_DEAD;
else
return COS_YIELD; /* initial state */
}
default: /* some error occurred */
return COS_DEAD;
}
}
}
static int luaB_costatus (lua_State *L) {
lua_State *co = getco(L);
lua_pushstring(L, statname[auxstatus(L, co)]);
return 1;
}
static int luaB_yieldable (lua_State *L) {
lua_State *co = lua_isnone(L, 1) ? L : getco(L);
lua_pushboolean(L, lua_isyieldable(co));
return 1;
}
static int luaB_corunning (lua_State *L) {
int ismain = lua_pushthread(L);
lua_pushboolean(L, ismain);
return 2;
}
static int luaB_close (lua_State *L) {
lua_State *co = getco(L);
int status = auxstatus(L, co);
switch (status) {
case COS_DEAD: case COS_YIELD: {
status = lua_closethread(co, L);
if (status == LUA_OK) {
lua_pushboolean(L, 1);
return 1;
}
else {
lua_pushboolean(L, 0);
lua_xmove(co, L, 1); /* move error message */
return 2;
}
}
default: /* normal or running coroutine */
return luaL_error(L, "cannot close a %s coroutine", statname[status]);
}
}
static const luaL_Reg co_funcs[] = {
{"create", luaB_cocreate},
{"resume", luaB_coresume},
{"running", luaB_corunning},
{"status", luaB_costatus},
{"wrap", luaB_cowrap},
{"yield", luaB_yield},
{"isyieldable", luaB_yieldable},
{"close", luaB_close},
{NULL, NULL}
};
LUAMOD_API int luaopen_coroutine (lua_State *L) {
luaL_newlib(L, co_funcs);
return 1;
}

View File

@@ -0,0 +1,64 @@
/*
** $Id: lctype.c $
** 'ctype' functions for Lua
** See Copyright Notice in lua.h
*/
#define lctype_c
#define LUA_CORE
#include "lprefix.h"
#include "lctype.h"
#if !LUA_USE_CTYPE /* { */
#include <limits.h>
#if defined (LUA_UCID) /* accept UniCode IDentifiers? */
/* consider all non-ascii codepoints to be alphabetic */
#define NONA 0x01
#else
#define NONA 0x00 /* default */
#endif
LUAI_DDEF const lu_byte luai_ctype_[UCHAR_MAX + 2] = {
0x00, /* EOZ */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0. */
0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 1. */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0c, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, /* 2. */
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, /* 3. */
0x16, 0x16, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
0x04, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x05, /* 4. */
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, /* 5. */
0x05, 0x05, 0x05, 0x04, 0x04, 0x04, 0x04, 0x05,
0x04, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x05, /* 6. */
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, /* 7. */
0x05, 0x05, 0x05, 0x04, 0x04, 0x04, 0x04, 0x00,
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* 8. */
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* 9. */
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* a. */
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* b. */
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
0x00, 0x00, NONA, NONA, NONA, NONA, NONA, NONA, /* c. */
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* d. */
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* e. */
NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA,
NONA, NONA, NONA, NONA, NONA, 0x00, 0x00, 0x00, /* f. */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
#endif /* } */

View File

@@ -0,0 +1,101 @@
/*
** $Id: lctype.h $
** 'ctype' functions for Lua
** See Copyright Notice in lua.h
*/
#ifndef lctype_h
#define lctype_h
#include "lua.h"
/*
** WARNING: the functions defined here do not necessarily correspond
** to the similar functions in the standard C ctype.h. They are
** optimized for the specific needs of Lua.
*/
#if !defined(LUA_USE_CTYPE)
#if 'A' == 65 && '0' == 48
/* ASCII case: can use its own tables; faster and fixed */
#define LUA_USE_CTYPE 0
#else
/* must use standard C ctype */
#define LUA_USE_CTYPE 1
#endif
#endif
#if !LUA_USE_CTYPE /* { */
#include <limits.h>
#include "llimits.h"
#define ALPHABIT 0
#define DIGITBIT 1
#define PRINTBIT 2
#define SPACEBIT 3
#define XDIGITBIT 4
#define MASK(B) (1 << (B))
/*
** add 1 to char to allow index -1 (EOZ)
*/
#define testprop(c,p) (luai_ctype_[(c)+1] & (p))
/*
** 'lalpha' (Lua alphabetic) and 'lalnum' (Lua alphanumeric) both include '_'
*/
#define lislalpha(c) testprop(c, MASK(ALPHABIT))
#define lislalnum(c) testprop(c, (MASK(ALPHABIT) | MASK(DIGITBIT)))
#define lisdigit(c) testprop(c, MASK(DIGITBIT))
#define lisspace(c) testprop(c, MASK(SPACEBIT))
#define lisprint(c) testprop(c, MASK(PRINTBIT))
#define lisxdigit(c) testprop(c, MASK(XDIGITBIT))
/*
** In ASCII, this 'ltolower' is correct for alphabetic characters and
** for '.'. That is enough for Lua needs. ('check_exp' ensures that
** the character either is an upper-case letter or is unchanged by
** the transformation, which holds for lower-case letters and '.'.)
*/
#define ltolower(c) \
check_exp(('A' <= (c) && (c) <= 'Z') || (c) == ((c) | ('A' ^ 'a')), \
(c) | ('A' ^ 'a'))
/* one entry for each character and for -1 (EOZ) */
LUAI_DDEC(const lu_byte luai_ctype_[UCHAR_MAX + 2];)
#else /* }{ */
/*
** use standard C ctypes
*/
#include <ctype.h>
#define lislalpha(c) (isalpha(c) || (c) == '_')
#define lislalnum(c) (isalnum(c) || (c) == '_')
#define lisdigit(c) (isdigit(c))
#define lisspace(c) (isspace(c))
#define lisprint(c) (isprint(c))
#define lisxdigit(c) (isxdigit(c))
#define ltolower(c) (tolower(c))
#endif /* } */
#endif

View File

@@ -0,0 +1,483 @@
/*
** $Id: ldblib.c $
** Interface from Lua to its debug API
** See Copyright Notice in lua.h
*/
#define ldblib_c
#define LUA_LIB
#include "lprefix.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
/*
** The hook table at registry[HOOKKEY] maps threads to their current
** hook function.
*/
static const char *const HOOKKEY = "_HOOKKEY";
/*
** If L1 != L, L1 can be in any state, and therefore there are no
** guarantees about its stack space; any push in L1 must be
** checked.
*/
static void checkstack (lua_State *L, lua_State *L1, int n) {
if (l_unlikely(L != L1 && !lua_checkstack(L1, n)))
luaL_error(L, "stack overflow");
}
static int db_getregistry (lua_State *L) {
lua_pushvalue(L, LUA_REGISTRYINDEX);
return 1;
}
static int db_getmetatable (lua_State *L) {
luaL_checkany(L, 1);
if (!lua_getmetatable(L, 1)) {
lua_pushnil(L); /* no metatable */
}
return 1;
}
static int db_setmetatable (lua_State *L) {
int t = lua_type(L, 2);
luaL_argexpected(L, t == LUA_TNIL || t == LUA_TTABLE, 2, "nil or table");
lua_settop(L, 2);
lua_setmetatable(L, 1);
return 1; /* return 1st argument */
}
static int db_getuservalue (lua_State *L) {
int n = (int)luaL_optinteger(L, 2, 1);
if (lua_type(L, 1) != LUA_TUSERDATA)
luaL_pushfail(L);
else if (lua_getiuservalue(L, 1, n) != LUA_TNONE) {
lua_pushboolean(L, 1);
return 2;
}
return 1;
}
static int db_setuservalue (lua_State *L) {
int n = (int)luaL_optinteger(L, 3, 1);
luaL_checktype(L, 1, LUA_TUSERDATA);
luaL_checkany(L, 2);
lua_settop(L, 2);
if (!lua_setiuservalue(L, 1, n))
luaL_pushfail(L);
return 1;
}
/*
** Auxiliary function used by several library functions: check for
** an optional thread as function's first argument and set 'arg' with
** 1 if this argument is present (so that functions can skip it to
** access their other arguments)
*/
static lua_State *getthread (lua_State *L, int *arg) {
if (lua_isthread(L, 1)) {
*arg = 1;
return lua_tothread(L, 1);
}
else {
*arg = 0;
return L; /* function will operate over current thread */
}
}
/*
** Variations of 'lua_settable', used by 'db_getinfo' to put results
** from 'lua_getinfo' into result table. Key is always a string;
** value can be a string, an int, or a boolean.
*/
static void settabss (lua_State *L, const char *k, const char *v) {
lua_pushstring(L, v);
lua_setfield(L, -2, k);
}
static void settabsi (lua_State *L, const char *k, int v) {
lua_pushinteger(L, v);
lua_setfield(L, -2, k);
}
static void settabsb (lua_State *L, const char *k, int v) {
lua_pushboolean(L, v);
lua_setfield(L, -2, k);
}
/*
** In function 'db_getinfo', the call to 'lua_getinfo' may push
** results on the stack; later it creates the result table to put
** these objects. Function 'treatstackoption' puts the result from
** 'lua_getinfo' on top of the result table so that it can call
** 'lua_setfield'.
*/
static void treatstackoption (lua_State *L, lua_State *L1, const char *fname) {
if (L == L1)
lua_rotate(L, -2, 1); /* exchange object and table */
else
lua_xmove(L1, L, 1); /* move object to the "main" stack */
lua_setfield(L, -2, fname); /* put object into table */
}
/*
** Calls 'lua_getinfo' and collects all results in a new table.
** L1 needs stack space for an optional input (function) plus
** two optional outputs (function and line table) from function
** 'lua_getinfo'.
*/
static int db_getinfo (lua_State *L) {
lua_Debug ar;
int arg;
lua_State *L1 = getthread(L, &arg);
const char *options = luaL_optstring(L, arg+2, "flnSrtu");
checkstack(L, L1, 3);
luaL_argcheck(L, options[0] != '>', arg + 2, "invalid option '>'");
if (lua_isfunction(L, arg + 1)) { /* info about a function? */
options = lua_pushfstring(L, ">%s", options); /* add '>' to 'options' */
lua_pushvalue(L, arg + 1); /* move function to 'L1' stack */
lua_xmove(L, L1, 1);
}
else { /* stack level */
if (!lua_getstack(L1, (int)luaL_checkinteger(L, arg + 1), &ar)) {
luaL_pushfail(L); /* level out of range */
return 1;
}
}
if (!lua_getinfo(L1, options, &ar))
return luaL_argerror(L, arg+2, "invalid option");
lua_newtable(L); /* table to collect results */
if (strchr(options, 'S')) {
lua_pushlstring(L, ar.source, ar.srclen);
lua_setfield(L, -2, "source");
settabss(L, "short_src", ar.short_src);
settabsi(L, "linedefined", ar.linedefined);
settabsi(L, "lastlinedefined", ar.lastlinedefined);
settabss(L, "what", ar.what);
}
if (strchr(options, 'l'))
settabsi(L, "currentline", ar.currentline);
if (strchr(options, 'u')) {
settabsi(L, "nups", ar.nups);
settabsi(L, "nparams", ar.nparams);
settabsb(L, "isvararg", ar.isvararg);
}
if (strchr(options, 'n')) {
settabss(L, "name", ar.name);
settabss(L, "namewhat", ar.namewhat);
}
if (strchr(options, 'r')) {
settabsi(L, "ftransfer", ar.ftransfer);
settabsi(L, "ntransfer", ar.ntransfer);
}
if (strchr(options, 't'))
settabsb(L, "istailcall", ar.istailcall);
if (strchr(options, 'L'))
treatstackoption(L, L1, "activelines");
if (strchr(options, 'f'))
treatstackoption(L, L1, "func");
return 1; /* return table */
}
static int db_getlocal (lua_State *L) {
int arg;
lua_State *L1 = getthread(L, &arg);
int nvar = (int)luaL_checkinteger(L, arg + 2); /* local-variable index */
if (lua_isfunction(L, arg + 1)) { /* function argument? */
lua_pushvalue(L, arg + 1); /* push function */
lua_pushstring(L, lua_getlocal(L, NULL, nvar)); /* push local name */
return 1; /* return only name (there is no value) */
}
else { /* stack-level argument */
lua_Debug ar;
const char *name;
int level = (int)luaL_checkinteger(L, arg + 1);
if (l_unlikely(!lua_getstack(L1, level, &ar))) /* out of range? */
return luaL_argerror(L, arg+1, "level out of range");
checkstack(L, L1, 1);
name = lua_getlocal(L1, &ar, nvar);
if (name) {
lua_xmove(L1, L, 1); /* move local value */
lua_pushstring(L, name); /* push name */
lua_rotate(L, -2, 1); /* re-order */
return 2;
}
else {
luaL_pushfail(L); /* no name (nor value) */
return 1;
}
}
}
static int db_setlocal (lua_State *L) {
int arg;
const char *name;
lua_State *L1 = getthread(L, &arg);
lua_Debug ar;
int level = (int)luaL_checkinteger(L, arg + 1);
int nvar = (int)luaL_checkinteger(L, arg + 2);
if (l_unlikely(!lua_getstack(L1, level, &ar))) /* out of range? */
return luaL_argerror(L, arg+1, "level out of range");
luaL_checkany(L, arg+3);
lua_settop(L, arg+3);
checkstack(L, L1, 1);
lua_xmove(L, L1, 1);
name = lua_setlocal(L1, &ar, nvar);
if (name == NULL)
lua_pop(L1, 1); /* pop value (if not popped by 'lua_setlocal') */
lua_pushstring(L, name);
return 1;
}
/*
** get (if 'get' is true) or set an upvalue from a closure
*/
static int auxupvalue (lua_State *L, int get) {
const char *name;
int n = (int)luaL_checkinteger(L, 2); /* upvalue index */
luaL_checktype(L, 1, LUA_TFUNCTION); /* closure */
name = get ? lua_getupvalue(L, 1, n) : lua_setupvalue(L, 1, n);
if (name == NULL) return 0;
lua_pushstring(L, name);
lua_insert(L, -(get+1)); /* no-op if get is false */
return get + 1;
}
static int db_getupvalue (lua_State *L) {
return auxupvalue(L, 1);
}
static int db_setupvalue (lua_State *L) {
luaL_checkany(L, 3);
return auxupvalue(L, 0);
}
/*
** Check whether a given upvalue from a given closure exists and
** returns its index
*/
static void *checkupval (lua_State *L, int argf, int argnup, int *pnup) {
void *id;
int nup = (int)luaL_checkinteger(L, argnup); /* upvalue index */
luaL_checktype(L, argf, LUA_TFUNCTION); /* closure */
id = lua_upvalueid(L, argf, nup);
if (pnup) {
luaL_argcheck(L, id != NULL, argnup, "invalid upvalue index");
*pnup = nup;
}
return id;
}
static int db_upvalueid (lua_State *L) {
void *id = checkupval(L, 1, 2, NULL);
if (id != NULL)
lua_pushlightuserdata(L, id);
else
luaL_pushfail(L);
return 1;
}
static int db_upvaluejoin (lua_State *L) {
int n1, n2;
checkupval(L, 1, 2, &n1);
checkupval(L, 3, 4, &n2);
luaL_argcheck(L, !lua_iscfunction(L, 1), 1, "Lua function expected");
luaL_argcheck(L, !lua_iscfunction(L, 3), 3, "Lua function expected");
lua_upvaluejoin(L, 1, n1, 3, n2);
return 0;
}
/*
** Call hook function registered at hook table for the current
** thread (if there is one)
*/
static void hookf (lua_State *L, lua_Debug *ar) {
static const char *const hooknames[] =
{"call", "return", "line", "count", "tail call"};
lua_getfield(L, LUA_REGISTRYINDEX, HOOKKEY);
lua_pushthread(L);
if (lua_rawget(L, -2) == LUA_TFUNCTION) { /* is there a hook function? */
lua_pushstring(L, hooknames[(int)ar->event]); /* push event name */
if (ar->currentline >= 0)
lua_pushinteger(L, ar->currentline); /* push current line */
else lua_pushnil(L);
lua_assert(lua_getinfo(L, "lS", ar));
lua_call(L, 2, 0); /* call hook function */
}
}
/*
** Convert a string mask (for 'sethook') into a bit mask
*/
static int makemask (const char *smask, int count) {
int mask = 0;
if (strchr(smask, 'c')) mask |= LUA_MASKCALL;
if (strchr(smask, 'r')) mask |= LUA_MASKRET;
if (strchr(smask, 'l')) mask |= LUA_MASKLINE;
if (count > 0) mask |= LUA_MASKCOUNT;
return mask;
}
/*
** Convert a bit mask (for 'gethook') into a string mask
*/
static char *unmakemask (int mask, char *smask) {
int i = 0;
if (mask & LUA_MASKCALL) smask[i++] = 'c';
if (mask & LUA_MASKRET) smask[i++] = 'r';
if (mask & LUA_MASKLINE) smask[i++] = 'l';
smask[i] = '\0';
return smask;
}
static int db_sethook (lua_State *L) {
int arg, mask, count;
lua_Hook func;
lua_State *L1 = getthread(L, &arg);
if (lua_isnoneornil(L, arg+1)) { /* no hook? */
lua_settop(L, arg+1);
func = NULL; mask = 0; count = 0; /* turn off hooks */
}
else {
const char *smask = luaL_checkstring(L, arg+2);
luaL_checktype(L, arg+1, LUA_TFUNCTION);
count = (int)luaL_optinteger(L, arg + 3, 0);
func = hookf; mask = makemask(smask, count);
}
if (!luaL_getsubtable(L, LUA_REGISTRYINDEX, HOOKKEY)) {
/* table just created; initialize it */
lua_pushliteral(L, "k");
lua_setfield(L, -2, "__mode"); /** hooktable.__mode = "k" */
lua_pushvalue(L, -1);
lua_setmetatable(L, -2); /* metatable(hooktable) = hooktable */
}
checkstack(L, L1, 1);
lua_pushthread(L1); lua_xmove(L1, L, 1); /* key (thread) */
lua_pushvalue(L, arg + 1); /* value (hook function) */
lua_rawset(L, -3); /* hooktable[L1] = new Lua hook */
lua_sethook(L1, func, mask, count);
return 0;
}
static int db_gethook (lua_State *L) {
int arg;
lua_State *L1 = getthread(L, &arg);
char buff[5];
int mask = lua_gethookmask(L1);
lua_Hook hook = lua_gethook(L1);
if (hook == NULL) { /* no hook? */
luaL_pushfail(L);
return 1;
}
else if (hook != hookf) /* external hook? */
lua_pushliteral(L, "external hook");
else { /* hook table must exist */
lua_getfield(L, LUA_REGISTRYINDEX, HOOKKEY);
checkstack(L, L1, 1);
lua_pushthread(L1); lua_xmove(L1, L, 1);
lua_rawget(L, -2); /* 1st result = hooktable[L1] */
lua_remove(L, -2); /* remove hook table */
}
lua_pushstring(L, unmakemask(mask, buff)); /* 2nd result = mask */
lua_pushinteger(L, lua_gethookcount(L1)); /* 3rd result = count */
return 3;
}
static int db_debug (lua_State *L) {
for (;;) {
char buffer[250];
lua_writestringerror("%s", "lua_debug> ");
if (fgets(buffer, sizeof(buffer), stdin) == NULL ||
strcmp(buffer, "cont\n") == 0)
return 0;
if (luaL_loadbuffer(L, buffer, strlen(buffer), "=(debug command)") ||
lua_pcall(L, 0, 0, 0))
lua_writestringerror("%s\n", luaL_tolstring(L, -1, NULL));
lua_settop(L, 0); /* remove eventual returns */
}
}
static int db_traceback (lua_State *L) {
int arg;
lua_State *L1 = getthread(L, &arg);
const char *msg = lua_tostring(L, arg + 1);
if (msg == NULL && !lua_isnoneornil(L, arg + 1)) /* non-string 'msg'? */
lua_pushvalue(L, arg + 1); /* return it untouched */
else {
int level = (int)luaL_optinteger(L, arg + 2, (L == L1) ? 1 : 0);
luaL_traceback(L, L1, msg, level);
}
return 1;
}
static int db_setcstacklimit (lua_State *L) {
int limit = (int)luaL_checkinteger(L, 1);
int res = lua_setcstacklimit(L, limit);
lua_pushinteger(L, res);
return 1;
}
static const luaL_Reg dblib[] = {
{"debug", db_debug},
{"getuservalue", db_getuservalue},
{"gethook", db_gethook},
{"getinfo", db_getinfo},
{"getlocal", db_getlocal},
{"getregistry", db_getregistry},
{"getmetatable", db_getmetatable},
{"getupvalue", db_getupvalue},
{"upvaluejoin", db_upvaluejoin},
{"upvalueid", db_upvalueid},
{"setuservalue", db_setuservalue},
{"sethook", db_sethook},
{"setlocal", db_setlocal},
{"setmetatable", db_setmetatable},
{"setupvalue", db_setupvalue},
{"traceback", db_traceback},
{"setcstacklimit", db_setcstacklimit},
{NULL, NULL}
};
LUAMOD_API int luaopen_debug (lua_State *L) {
luaL_newlib(L, dblib);
return 1;
}

View File

@@ -0,0 +1,962 @@
/*
** $Id: ldebug.c $
** Debug Interface
** See Copyright Notice in lua.h
*/
#define ldebug_c
#define LUA_CORE
#include "lprefix.h"
#include <stdarg.h>
#include <stddef.h>
#include <string.h>
#include "lua.h"
#include "lapi.h"
#include "lcode.h"
#include "ldebug.h"
#include "ldo.h"
#include "lfunc.h"
#include "lobject.h"
#include "lopcodes.h"
#include "lstate.h"
#include "lstring.h"
#include "ltable.h"
#include "ltm.h"
#include "lvm.h"
#define LuaClosure(f) ((f) != NULL && (f)->c.tt == LUA_VLCL)
static const char *funcnamefromcall (lua_State *L, CallInfo *ci,
const char **name);
static int currentpc (CallInfo *ci) {
lua_assert(isLua(ci));
return pcRel(ci->u.l.savedpc, ci_func(ci)->p);
}
/*
** Get a "base line" to find the line corresponding to an instruction.
** Base lines are regularly placed at MAXIWTHABS intervals, so usually
** an integer division gets the right place. When the source file has
** large sequences of empty/comment lines, it may need extra entries,
** so the original estimate needs a correction.
** If the original estimate is -1, the initial 'if' ensures that the
** 'while' will run at least once.
** The assertion that the estimate is a lower bound for the correct base
** is valid as long as the debug info has been generated with the same
** value for MAXIWTHABS or smaller. (Previous releases use a little
** smaller value.)
*/
static int getbaseline (const Proto *f, int pc, int *basepc) {
if (f->sizeabslineinfo == 0 || pc < f->abslineinfo[0].pc) {
*basepc = -1; /* start from the beginning */
return f->linedefined;
}
else {
int i = cast_uint(pc) / MAXIWTHABS - 1; /* get an estimate */
/* estimate must be a lower bound of the correct base */
lua_assert(i < 0 ||
(i < f->sizeabslineinfo && f->abslineinfo[i].pc <= pc));
while (i + 1 < f->sizeabslineinfo && pc >= f->abslineinfo[i + 1].pc)
i++; /* low estimate; adjust it */
*basepc = f->abslineinfo[i].pc;
return f->abslineinfo[i].line;
}
}
/*
** Get the line corresponding to instruction 'pc' in function 'f';
** first gets a base line and from there does the increments until
** the desired instruction.
*/
int luaG_getfuncline (const Proto *f, int pc) {
if (f->lineinfo == NULL) /* no debug information? */
return -1;
else {
int basepc;
int baseline = getbaseline(f, pc, &basepc);
while (basepc++ < pc) { /* walk until given instruction */
lua_assert(f->lineinfo[basepc] != ABSLINEINFO);
baseline += f->lineinfo[basepc]; /* correct line */
}
return baseline;
}
}
static int getcurrentline (CallInfo *ci) {
return luaG_getfuncline(ci_func(ci)->p, currentpc(ci));
}
/*
** Set 'trap' for all active Lua frames.
** This function can be called during a signal, under "reasonable"
** assumptions. A new 'ci' is completely linked in the list before it
** becomes part of the "active" list, and we assume that pointers are
** atomic; see comment in next function.
** (A compiler doing interprocedural optimizations could, theoretically,
** reorder memory writes in such a way that the list could be
** temporarily broken while inserting a new element. We simply assume it
** has no good reasons to do that.)
*/
static void settraps (CallInfo *ci) {
for (; ci != NULL; ci = ci->previous)
if (isLua(ci))
ci->u.l.trap = 1;
}
/*
** This function can be called during a signal, under "reasonable"
** assumptions.
** Fields 'basehookcount' and 'hookcount' (set by 'resethookcount')
** are for debug only, and it is no problem if they get arbitrary
** values (causes at most one wrong hook call). 'hookmask' is an atomic
** value. We assume that pointers are atomic too (e.g., gcc ensures that
** for all platforms where it runs). Moreover, 'hook' is always checked
** before being called (see 'luaD_hook').
*/
LUA_API void lua_sethook (lua_State *L, lua_Hook func, int mask, int count) {
if (func == NULL || mask == 0) { /* turn off hooks? */
mask = 0;
func = NULL;
}
L->hook = func;
L->basehookcount = count;
resethookcount(L);
L->hookmask = cast_byte(mask);
if (mask)
settraps(L->ci); /* to trace inside 'luaV_execute' */
}
LUA_API lua_Hook lua_gethook (lua_State *L) {
return L->hook;
}
LUA_API int lua_gethookmask (lua_State *L) {
return L->hookmask;
}
LUA_API int lua_gethookcount (lua_State *L) {
return L->basehookcount;
}
LUA_API int lua_getstack (lua_State *L, int level, lua_Debug *ar) {
int status;
CallInfo *ci;
if (level < 0) return 0; /* invalid (negative) level */
lua_lock(L);
for (ci = L->ci; level > 0 && ci != &L->base_ci; ci = ci->previous)
level--;
if (level == 0 && ci != &L->base_ci) { /* level found? */
status = 1;
ar->i_ci = ci;
}
else status = 0; /* no such level */
lua_unlock(L);
return status;
}
static const char *upvalname (const Proto *p, int uv) {
TString *s = check_exp(uv < p->sizeupvalues, p->upvalues[uv].name);
if (s == NULL) return "?";
else return getstr(s);
}
static const char *findvararg (CallInfo *ci, int n, StkId *pos) {
if (clLvalue(s2v(ci->func.p))->p->is_vararg) {
int nextra = ci->u.l.nextraargs;
if (n >= -nextra) { /* 'n' is negative */
*pos = ci->func.p - nextra - (n + 1);
return "(vararg)"; /* generic name for any vararg */
}
}
return NULL; /* no such vararg */
}
const char *luaG_findlocal (lua_State *L, CallInfo *ci, int n, StkId *pos) {
StkId base = ci->func.p + 1;
const char *name = NULL;
if (isLua(ci)) {
if (n < 0) /* access to vararg values? */
return findvararg(ci, n, pos);
else
name = luaF_getlocalname(ci_func(ci)->p, n, currentpc(ci));
}
if (name == NULL) { /* no 'standard' name? */
StkId limit = (ci == L->ci) ? L->top.p : ci->next->func.p;
if (limit - base >= n && n > 0) { /* is 'n' inside 'ci' stack? */
/* generic name for any valid slot */
name = isLua(ci) ? "(temporary)" : "(C temporary)";
}
else
return NULL; /* no name */
}
if (pos)
*pos = base + (n - 1);
return name;
}
LUA_API const char *lua_getlocal (lua_State *L, const lua_Debug *ar, int n) {
const char *name;
lua_lock(L);
if (ar == NULL) { /* information about non-active function? */
if (!isLfunction(s2v(L->top.p - 1))) /* not a Lua function? */
name = NULL;
else /* consider live variables at function start (parameters) */
name = luaF_getlocalname(clLvalue(s2v(L->top.p - 1))->p, n, 0);
}
else { /* active function; get information through 'ar' */
StkId pos = NULL; /* to avoid warnings */
name = luaG_findlocal(L, ar->i_ci, n, &pos);
if (name) {
setobjs2s(L, L->top.p, pos);
api_incr_top(L);
}
}
lua_unlock(L);
return name;
}
LUA_API const char *lua_setlocal (lua_State *L, const lua_Debug *ar, int n) {
StkId pos = NULL; /* to avoid warnings */
const char *name;
lua_lock(L);
name = luaG_findlocal(L, ar->i_ci, n, &pos);
if (name) {
setobjs2s(L, pos, L->top.p - 1);
L->top.p--; /* pop value */
}
lua_unlock(L);
return name;
}
static void funcinfo (lua_Debug *ar, Closure *cl) {
if (!LuaClosure(cl)) {
ar->source = "=[C]";
ar->srclen = LL("=[C]");
ar->linedefined = -1;
ar->lastlinedefined = -1;
ar->what = "C";
}
else {
const Proto *p = cl->l.p;
if (p->source) {
ar->source = getstr(p->source);
ar->srclen = tsslen(p->source);
}
else {
ar->source = "=?";
ar->srclen = LL("=?");
}
ar->linedefined = p->linedefined;
ar->lastlinedefined = p->lastlinedefined;
ar->what = (ar->linedefined == 0) ? "main" : "Lua";
}
luaO_chunkid(ar->short_src, ar->source, ar->srclen);
}
static int nextline (const Proto *p, int currentline, int pc) {
if (p->lineinfo[pc] != ABSLINEINFO)
return currentline + p->lineinfo[pc];
else
return luaG_getfuncline(p, pc);
}
static void collectvalidlines (lua_State *L, Closure *f) {
if (!LuaClosure(f)) {
setnilvalue(s2v(L->top.p));
api_incr_top(L);
}
else {
const Proto *p = f->l.p;
int currentline = p->linedefined;
Table *t = luaH_new(L); /* new table to store active lines */
sethvalue2s(L, L->top.p, t); /* push it on stack */
api_incr_top(L);
if (p->lineinfo != NULL) { /* proto with debug information? */
int i;
TValue v;
setbtvalue(&v); /* boolean 'true' to be the value of all indices */
if (!p->is_vararg) /* regular function? */
i = 0; /* consider all instructions */
else { /* vararg function */
lua_assert(GET_OPCODE(p->code[0]) == OP_VARARGPREP);
currentline = nextline(p, currentline, 0);
i = 1; /* skip first instruction (OP_VARARGPREP) */
}
for (; i < p->sizelineinfo; i++) { /* for each instruction */
currentline = nextline(p, currentline, i); /* get its line */
luaH_setint(L, t, currentline, &v); /* table[line] = true */
}
}
}
}
static const char *getfuncname (lua_State *L, CallInfo *ci, const char **name) {
/* calling function is a known function? */
if (ci != NULL && !(ci->callstatus & CIST_TAIL))
return funcnamefromcall(L, ci->previous, name);
else return NULL; /* no way to find a name */
}
static int auxgetinfo (lua_State *L, const char *what, lua_Debug *ar,
Closure *f, CallInfo *ci) {
int status = 1;
for (; *what; what++) {
switch (*what) {
case 'S': {
funcinfo(ar, f);
break;
}
case 'l': {
ar->currentline = (ci && isLua(ci)) ? getcurrentline(ci) : -1;
break;
}
case 'u': {
ar->nups = (f == NULL) ? 0 : f->c.nupvalues;
if (!LuaClosure(f)) {
ar->isvararg = 1;
ar->nparams = 0;
}
else {
ar->isvararg = f->l.p->is_vararg;
ar->nparams = f->l.p->numparams;
}
break;
}
case 't': {
ar->istailcall = (ci) ? ci->callstatus & CIST_TAIL : 0;
break;
}
case 'n': {
ar->namewhat = getfuncname(L, ci, &ar->name);
if (ar->namewhat == NULL) {
ar->namewhat = ""; /* not found */
ar->name = NULL;
}
break;
}
case 'r': {
if (ci == NULL || !(ci->callstatus & CIST_TRAN))
ar->ftransfer = ar->ntransfer = 0;
else {
ar->ftransfer = ci->u2.transferinfo.ftransfer;
ar->ntransfer = ci->u2.transferinfo.ntransfer;
}
break;
}
case 'L':
case 'f': /* handled by lua_getinfo */
break;
default: status = 0; /* invalid option */
}
}
return status;
}
LUA_API int lua_getinfo (lua_State *L, const char *what, lua_Debug *ar) {
int status;
Closure *cl;
CallInfo *ci;
TValue *func;
lua_lock(L);
if (*what == '>') {
ci = NULL;
func = s2v(L->top.p - 1);
api_check(L, ttisfunction(func), "function expected");
what++; /* skip the '>' */
L->top.p--; /* pop function */
}
else {
ci = ar->i_ci;
func = s2v(ci->func.p);
lua_assert(ttisfunction(func));
}
cl = ttisclosure(func) ? clvalue(func) : NULL;
status = auxgetinfo(L, what, ar, cl, ci);
if (strchr(what, 'f')) {
setobj2s(L, L->top.p, func);
api_incr_top(L);
}
if (strchr(what, 'L'))
collectvalidlines(L, cl);
lua_unlock(L);
return status;
}
/*
** {======================================================
** Symbolic Execution
** =======================================================
*/
static int filterpc (int pc, int jmptarget) {
if (pc < jmptarget) /* is code conditional (inside a jump)? */
return -1; /* cannot know who sets that register */
else return pc; /* current position sets that register */
}
/*
** Try to find last instruction before 'lastpc' that modified register 'reg'.
*/
static int findsetreg (const Proto *p, int lastpc, int reg) {
int pc;
int setreg = -1; /* keep last instruction that changed 'reg' */
int jmptarget = 0; /* any code before this address is conditional */
if (testMMMode(GET_OPCODE(p->code[lastpc])))
lastpc--; /* previous instruction was not actually executed */
for (pc = 0; pc < lastpc; pc++) {
Instruction i = p->code[pc];
OpCode op = GET_OPCODE(i);
int a = GETARG_A(i);
int change; /* true if current instruction changed 'reg' */
switch (op) {
case OP_LOADNIL: { /* set registers from 'a' to 'a+b' */
int b = GETARG_B(i);
change = (a <= reg && reg <= a + b);
break;
}
case OP_TFORCALL: { /* affect all regs above its base */
change = (reg >= a + 2);
break;
}
case OP_CALL:
case OP_TAILCALL: { /* affect all registers above base */
change = (reg >= a);
break;
}
case OP_JMP: { /* doesn't change registers, but changes 'jmptarget' */
int b = GETARG_sJ(i);
int dest = pc + 1 + b;
/* jump does not skip 'lastpc' and is larger than current one? */
if (dest <= lastpc && dest > jmptarget)
jmptarget = dest; /* update 'jmptarget' */
change = 0;
break;
}
default: /* any instruction that sets A */
change = (testAMode(op) && reg == a);
break;
}
if (change)
setreg = filterpc(pc, jmptarget);
}
return setreg;
}
/*
** Find a "name" for the constant 'c'.
*/
static const char *kname (const Proto *p, int index, const char **name) {
TValue *kvalue = &p->k[index];
if (ttisstring(kvalue)) {
*name = getstr(tsvalue(kvalue));
return "constant";
}
else {
*name = "?";
return NULL;
}
}
static const char *basicgetobjname (const Proto *p, int *ppc, int reg,
const char **name) {
int pc = *ppc;
*name = luaF_getlocalname(p, reg + 1, pc);
if (*name) /* is a local? */
return "local";
/* else try symbolic execution */
*ppc = pc = findsetreg(p, pc, reg);
if (pc != -1) { /* could find instruction? */
Instruction i = p->code[pc];
OpCode op = GET_OPCODE(i);
switch (op) {
case OP_MOVE: {
int b = GETARG_B(i); /* move from 'b' to 'a' */
if (b < GETARG_A(i))
return basicgetobjname(p, ppc, b, name); /* get name for 'b' */
break;
}
case OP_GETUPVAL: {
*name = upvalname(p, GETARG_B(i));
return "upvalue";
}
case OP_LOADK: return kname(p, GETARG_Bx(i), name);
case OP_LOADKX: return kname(p, GETARG_Ax(p->code[pc + 1]), name);
default: break;
}
}
return NULL; /* could not find reasonable name */
}
/*
** Find a "name" for the register 'c'.
*/
static void rname (const Proto *p, int pc, int c, const char **name) {
const char *what = basicgetobjname(p, &pc, c, name); /* search for 'c' */
if (!(what && *what == 'c')) /* did not find a constant name? */
*name = "?";
}
/*
** Find a "name" for a 'C' value in an RK instruction.
*/
static void rkname (const Proto *p, int pc, Instruction i, const char **name) {
int c = GETARG_C(i); /* key index */
if (GETARG_k(i)) /* is 'c' a constant? */
kname(p, c, name);
else /* 'c' is a register */
rname(p, pc, c, name);
}
/*
** Check whether table being indexed by instruction 'i' is the
** environment '_ENV'
*/
static const char *isEnv (const Proto *p, int pc, Instruction i, int isup) {
int t = GETARG_B(i); /* table index */
const char *name; /* name of indexed variable */
if (isup) /* is 't' an upvalue? */
name = upvalname(p, t);
else /* 't' is a register */
basicgetobjname(p, &pc, t, &name);
return (name && strcmp(name, LUA_ENV) == 0) ? "global" : "field";
}
/*
** Extend 'basicgetobjname' to handle table accesses
*/
static const char *getobjname (const Proto *p, int lastpc, int reg,
const char **name) {
const char *kind = basicgetobjname(p, &lastpc, reg, name);
if (kind != NULL)
return kind;
else if (lastpc != -1) { /* could find instruction? */
Instruction i = p->code[lastpc];
OpCode op = GET_OPCODE(i);
switch (op) {
case OP_GETTABUP: {
int k = GETARG_C(i); /* key index */
kname(p, k, name);
return isEnv(p, lastpc, i, 1);
}
case OP_GETTABLE: {
int k = GETARG_C(i); /* key index */
rname(p, lastpc, k, name);
return isEnv(p, lastpc, i, 0);
}
case OP_GETI: {
*name = "integer index";
return "field";
}
case OP_GETFIELD: {
int k = GETARG_C(i); /* key index */
kname(p, k, name);
return isEnv(p, lastpc, i, 0);
}
case OP_SELF: {
rkname(p, lastpc, i, name);
return "method";
}
default: break; /* go through to return NULL */
}
}
return NULL; /* could not find reasonable name */
}
/*
** Try to find a name for a function based on the code that called it.
** (Only works when function was called by a Lua function.)
** Returns what the name is (e.g., "for iterator", "method",
** "metamethod") and sets '*name' to point to the name.
*/
static const char *funcnamefromcode (lua_State *L, const Proto *p,
int pc, const char **name) {
TMS tm = (TMS)0; /* (initial value avoids warnings) */
Instruction i = p->code[pc]; /* calling instruction */
switch (GET_OPCODE(i)) {
case OP_CALL:
case OP_TAILCALL:
return getobjname(p, pc, GETARG_A(i), name); /* get function name */
case OP_TFORCALL: { /* for iterator */
*name = "for iterator";
return "for iterator";
}
/* other instructions can do calls through metamethods */
case OP_SELF: case OP_GETTABUP: case OP_GETTABLE:
case OP_GETI: case OP_GETFIELD:
tm = TM_INDEX;
break;
case OP_SETTABUP: case OP_SETTABLE: case OP_SETI: case OP_SETFIELD:
tm = TM_NEWINDEX;
break;
case OP_MMBIN: case OP_MMBINI: case OP_MMBINK: {
tm = cast(TMS, GETARG_C(i));
break;
}
case OP_UNM: tm = TM_UNM; break;
case OP_BNOT: tm = TM_BNOT; break;
case OP_LEN: tm = TM_LEN; break;
case OP_CONCAT: tm = TM_CONCAT; break;
case OP_EQ: tm = TM_EQ; break;
/* no cases for OP_EQI and OP_EQK, as they don't call metamethods */
case OP_LT: case OP_LTI: case OP_GTI: tm = TM_LT; break;
case OP_LE: case OP_LEI: case OP_GEI: tm = TM_LE; break;
case OP_CLOSE: case OP_RETURN: tm = TM_CLOSE; break;
default:
return NULL; /* cannot find a reasonable name */
}
*name = getshrstr(G(L)->tmname[tm]) + 2;
return "metamethod";
}
/*
** Try to find a name for a function based on how it was called.
*/
static const char *funcnamefromcall (lua_State *L, CallInfo *ci,
const char **name) {
if (ci->callstatus & CIST_HOOKED) { /* was it called inside a hook? */
*name = "?";
return "hook";
}
else if (ci->callstatus & CIST_FIN) { /* was it called as a finalizer? */
*name = "__gc";
return "metamethod"; /* report it as such */
}
else if (isLua(ci))
return funcnamefromcode(L, ci_func(ci)->p, currentpc(ci), name);
else
return NULL;
}
/* }====================================================== */
/*
** Check whether pointer 'o' points to some value in the stack frame of
** the current function and, if so, returns its index. Because 'o' may
** not point to a value in this stack, we cannot compare it with the
** region boundaries (undefined behavior in ISO C).
*/
static int instack (CallInfo *ci, const TValue *o) {
int pos;
StkId base = ci->func.p + 1;
for (pos = 0; base + pos < ci->top.p; pos++) {
if (o == s2v(base + pos))
return pos;
}
return -1; /* not found */
}
/*
** Checks whether value 'o' came from an upvalue. (That can only happen
** with instructions OP_GETTABUP/OP_SETTABUP, which operate directly on
** upvalues.)
*/
static const char *getupvalname (CallInfo *ci, const TValue *o,
const char **name) {
LClosure *c = ci_func(ci);
int i;
for (i = 0; i < c->nupvalues; i++) {
if (c->upvals[i]->v.p == o) {
*name = upvalname(c->p, i);
return "upvalue";
}
}
return NULL;
}
static const char *formatvarinfo (lua_State *L, const char *kind,
const char *name) {
if (kind == NULL)
return ""; /* no information */
else
return luaO_pushfstring(L, " (%s '%s')", kind, name);
}
/*
** Build a string with a "description" for the value 'o', such as
** "variable 'x'" or "upvalue 'y'".
*/
static const char *varinfo (lua_State *L, const TValue *o) {
CallInfo *ci = L->ci;
const char *name = NULL; /* to avoid warnings */
const char *kind = NULL;
if (isLua(ci)) {
kind = getupvalname(ci, o, &name); /* check whether 'o' is an upvalue */
if (!kind) { /* not an upvalue? */
int reg = instack(ci, o); /* try a register */
if (reg >= 0) /* is 'o' a register? */
kind = getobjname(ci_func(ci)->p, currentpc(ci), reg, &name);
}
}
return formatvarinfo(L, kind, name);
}
/*
** Raise a type error
*/
static l_noret typeerror (lua_State *L, const TValue *o, const char *op,
const char *extra) {
const char *t = luaT_objtypename(L, o);
luaG_runerror(L, "attempt to %s a %s value%s", op, t, extra);
}
/*
** Raise a type error with "standard" information about the faulty
** object 'o' (using 'varinfo').
*/
l_noret luaG_typeerror (lua_State *L, const TValue *o, const char *op) {
typeerror(L, o, op, varinfo(L, o));
}
/*
** Raise an error for calling a non-callable object. Try to find a name
** for the object based on how it was called ('funcnamefromcall'); if it
** cannot get a name there, try 'varinfo'.
*/
l_noret luaG_callerror (lua_State *L, const TValue *o) {
CallInfo *ci = L->ci;
const char *name = NULL; /* to avoid warnings */
const char *kind = funcnamefromcall(L, ci, &name);
const char *extra = kind ? formatvarinfo(L, kind, name) : varinfo(L, o);
typeerror(L, o, "call", extra);
}
l_noret luaG_forerror (lua_State *L, const TValue *o, const char *what) {
luaG_runerror(L, "bad 'for' %s (number expected, got %s)",
what, luaT_objtypename(L, o));
}
l_noret luaG_concaterror (lua_State *L, const TValue *p1, const TValue *p2) {
if (ttisstring(p1) || cvt2str(p1)) p1 = p2;
luaG_typeerror(L, p1, "concatenate");
}
l_noret luaG_opinterror (lua_State *L, const TValue *p1,
const TValue *p2, const char *msg) {
if (!ttisnumber(p1)) /* first operand is wrong? */
p2 = p1; /* now second is wrong */
luaG_typeerror(L, p2, msg);
}
/*
** Error when both values are convertible to numbers, but not to integers
*/
l_noret luaG_tointerror (lua_State *L, const TValue *p1, const TValue *p2) {
lua_Integer temp;
if (!luaV_tointegerns(p1, &temp, LUA_FLOORN2I))
p2 = p1;
luaG_runerror(L, "number%s has no integer representation", varinfo(L, p2));
}
l_noret luaG_ordererror (lua_State *L, const TValue *p1, const TValue *p2) {
const char *t1 = luaT_objtypename(L, p1);
const char *t2 = luaT_objtypename(L, p2);
if (strcmp(t1, t2) == 0)
luaG_runerror(L, "attempt to compare two %s values", t1);
else
luaG_runerror(L, "attempt to compare %s with %s", t1, t2);
}
/* add src:line information to 'msg' */
const char *luaG_addinfo (lua_State *L, const char *msg, TString *src,
int line) {
char buff[LUA_IDSIZE];
if (src)
luaO_chunkid(buff, getstr(src), tsslen(src));
else { /* no source available; use "?" instead */
buff[0] = '?'; buff[1] = '\0';
}
return luaO_pushfstring(L, "%s:%d: %s", buff, line, msg);
}
l_noret luaG_errormsg (lua_State *L) {
if (L->errfunc != 0) { /* is there an error handling function? */
StkId errfunc = restorestack(L, L->errfunc);
lua_assert(ttisfunction(s2v(errfunc)));
setobjs2s(L, L->top.p, L->top.p - 1); /* move argument */
setobjs2s(L, L->top.p - 1, errfunc); /* push function */
L->top.p++; /* assume EXTRA_STACK */
luaD_callnoyield(L, L->top.p - 2, 1); /* call it */
}
luaD_throw(L, LUA_ERRRUN);
}
l_noret luaG_runerror (lua_State *L, const char *fmt, ...) {
CallInfo *ci = L->ci;
const char *msg;
va_list argp;
luaC_checkGC(L); /* error message uses memory */
va_start(argp, fmt);
msg = luaO_pushvfstring(L, fmt, argp); /* format message */
va_end(argp);
if (isLua(ci)) { /* if Lua function, add source:line information */
luaG_addinfo(L, msg, ci_func(ci)->p->source, getcurrentline(ci));
setobjs2s(L, L->top.p - 2, L->top.p - 1); /* remove 'msg' */
L->top.p--;
}
luaG_errormsg(L);
}
/*
** Check whether new instruction 'newpc' is in a different line from
** previous instruction 'oldpc'. More often than not, 'newpc' is only
** one or a few instructions after 'oldpc' (it must be after, see
** caller), so try to avoid calling 'luaG_getfuncline'. If they are
** too far apart, there is a good chance of a ABSLINEINFO in the way,
** so it goes directly to 'luaG_getfuncline'.
*/
static int changedline (const Proto *p, int oldpc, int newpc) {
if (p->lineinfo == NULL) /* no debug information? */
return 0;
if (newpc - oldpc < MAXIWTHABS / 2) { /* not too far apart? */
int delta = 0; /* line difference */
int pc = oldpc;
for (;;) {
int lineinfo = p->lineinfo[++pc];
if (lineinfo == ABSLINEINFO)
break; /* cannot compute delta; fall through */
delta += lineinfo;
if (pc == newpc)
return (delta != 0); /* delta computed successfully */
}
}
/* either instructions are too far apart or there is an absolute line
info in the way; compute line difference explicitly */
return (luaG_getfuncline(p, oldpc) != luaG_getfuncline(p, newpc));
}
/*
** Traces Lua calls. If code is running the first instruction of a function,
** and function is not vararg, and it is not coming from an yield,
** calls 'luaD_hookcall'. (Vararg functions will call 'luaD_hookcall'
** after adjusting its variable arguments; otherwise, they could call
** a line/count hook before the call hook. Functions coming from
** an yield already called 'luaD_hookcall' before yielding.)
*/
int luaG_tracecall (lua_State *L) {
CallInfo *ci = L->ci;
Proto *p = ci_func(ci)->p;
ci->u.l.trap = 1; /* ensure hooks will be checked */
if (ci->u.l.savedpc == p->code) { /* first instruction (not resuming)? */
if (p->is_vararg)
return 0; /* hooks will start at VARARGPREP instruction */
else if (!(ci->callstatus & CIST_HOOKYIELD)) /* not yieded? */
luaD_hookcall(L, ci); /* check 'call' hook */
}
return 1; /* keep 'trap' on */
}
/*
** Traces the execution of a Lua function. Called before the execution
** of each opcode, when debug is on. 'L->oldpc' stores the last
** instruction traced, to detect line changes. When entering a new
** function, 'npci' will be zero and will test as a new line whatever
** the value of 'oldpc'. Some exceptional conditions may return to
** a function without setting 'oldpc'. In that case, 'oldpc' may be
** invalid; if so, use zero as a valid value. (A wrong but valid 'oldpc'
** at most causes an extra call to a line hook.)
** This function is not "Protected" when called, so it should correct
** 'L->top.p' before calling anything that can run the GC.
*/
int luaG_traceexec (lua_State *L, const Instruction *pc) {
CallInfo *ci = L->ci;
lu_byte mask = L->hookmask;
const Proto *p = ci_func(ci)->p;
int counthook;
if (!(mask & (LUA_MASKLINE | LUA_MASKCOUNT))) { /* no hooks? */
ci->u.l.trap = 0; /* don't need to stop again */
return 0; /* turn off 'trap' */
}
pc++; /* reference is always next instruction */
ci->u.l.savedpc = pc; /* save 'pc' */
counthook = (mask & LUA_MASKCOUNT) && (--L->hookcount == 0);
if (counthook)
resethookcount(L); /* reset count */
else if (!(mask & LUA_MASKLINE))
return 1; /* no line hook and count != 0; nothing to be done now */
if (ci->callstatus & CIST_HOOKYIELD) { /* hook yielded last time? */
ci->callstatus &= ~CIST_HOOKYIELD; /* erase mark */
return 1; /* do not call hook again (VM yielded, so it did not move) */
}
if (!isIT(*(ci->u.l.savedpc - 1))) /* top not being used? */
L->top.p = ci->top.p; /* correct top */
if (counthook)
luaD_hook(L, LUA_HOOKCOUNT, -1, 0, 0); /* call count hook */
if (mask & LUA_MASKLINE) {
/* 'L->oldpc' may be invalid; use zero in this case */
int oldpc = (L->oldpc < p->sizecode) ? L->oldpc : 0;
int npci = pcRel(pc, p);
if (npci <= oldpc || /* call hook when jump back (loop), */
changedline(p, oldpc, npci)) { /* or when enter new line */
int newline = luaG_getfuncline(p, npci);
luaD_hook(L, LUA_HOOKLINE, newline, 0, 0); /* call line hook */
}
L->oldpc = npci; /* 'pc' of last call to line hook */
}
if (L->status == LUA_YIELD) { /* did hook yield? */
if (counthook)
L->hookcount = 1; /* undo decrement to zero */
ci->callstatus |= CIST_HOOKYIELD; /* mark that it yielded */
luaD_throw(L, LUA_YIELD);
}
return 1; /* keep 'trap' on */
}

View File

@@ -0,0 +1,64 @@
/*
** $Id: ldebug.h $
** Auxiliary functions from Debug Interface module
** See Copyright Notice in lua.h
*/
#ifndef ldebug_h
#define ldebug_h
#include "lstate.h"
#define pcRel(pc, p) (cast_int((pc) - (p)->code) - 1)
/* Active Lua function (given call info) */
#define ci_func(ci) (clLvalue(s2v((ci)->func.p)))
#define resethookcount(L) (L->hookcount = L->basehookcount)
/*
** mark for entries in 'lineinfo' array that has absolute information in
** 'abslineinfo' array
*/
#define ABSLINEINFO (-0x80)
/*
** MAXimum number of successive Instructions WiTHout ABSolute line
** information. (A power of two allows fast divisions.)
*/
#if !defined(MAXIWTHABS)
#define MAXIWTHABS 128
#endif
LUAI_FUNC int luaG_getfuncline (const Proto *f, int pc);
LUAI_FUNC const char *luaG_findlocal (lua_State *L, CallInfo *ci, int n,
StkId *pos);
LUAI_FUNC l_noret luaG_typeerror (lua_State *L, const TValue *o,
const char *opname);
LUAI_FUNC l_noret luaG_callerror (lua_State *L, const TValue *o);
LUAI_FUNC l_noret luaG_forerror (lua_State *L, const TValue *o,
const char *what);
LUAI_FUNC l_noret luaG_concaterror (lua_State *L, const TValue *p1,
const TValue *p2);
LUAI_FUNC l_noret luaG_opinterror (lua_State *L, const TValue *p1,
const TValue *p2,
const char *msg);
LUAI_FUNC l_noret luaG_tointerror (lua_State *L, const TValue *p1,
const TValue *p2);
LUAI_FUNC l_noret luaG_ordererror (lua_State *L, const TValue *p1,
const TValue *p2);
LUAI_FUNC l_noret luaG_runerror (lua_State *L, const char *fmt, ...);
LUAI_FUNC const char *luaG_addinfo (lua_State *L, const char *msg,
TString *src, int line);
LUAI_FUNC l_noret luaG_errormsg (lua_State *L);
LUAI_FUNC int luaG_traceexec (lua_State *L, const Instruction *pc);
LUAI_FUNC int luaG_tracecall (lua_State *L);
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,87 @@
/*
** $Id: ldo.h $
** Stack and Call structure of Lua
** See Copyright Notice in lua.h
*/
#ifndef ldo_h
#define ldo_h
#include "llimits.h"
#include "lobject.h"
#include "lstate.h"
#include "lzio.h"
/*
** Macro to check stack size and grow stack if needed. Parameters
** 'pre'/'pos' allow the macro to preserve a pointer into the
** stack across reallocations, doing the work only when needed.
** It also allows the running of one GC step when the stack is
** reallocated.
** 'condmovestack' is used in heavy tests to force a stack reallocation
** at every check.
*/
#define luaD_checkstackaux(L,n,pre,pos) \
if (l_unlikely(L->stack_last.p - L->top.p <= (n))) \
{ pre; luaD_growstack(L, n, 1); pos; } \
else { condmovestack(L,pre,pos); }
/* In general, 'pre'/'pos' are empty (nothing to save) */
#define luaD_checkstack(L,n) luaD_checkstackaux(L,n,(void)0,(void)0)
#define savestack(L,pt) (cast_charp(pt) - cast_charp(L->stack.p))
#define restorestack(L,n) cast(StkId, cast_charp(L->stack.p) + (n))
/* macro to check stack size, preserving 'p' */
#define checkstackp(L,n,p) \
luaD_checkstackaux(L, n, \
ptrdiff_t t__ = savestack(L, p), /* save 'p' */ \
p = restorestack(L, t__)) /* 'pos' part: restore 'p' */
/* macro to check stack size and GC, preserving 'p' */
#define checkstackGCp(L,n,p) \
luaD_checkstackaux(L, n, \
ptrdiff_t t__ = savestack(L, p); /* save 'p' */ \
luaC_checkGC(L), /* stack grow uses memory */ \
p = restorestack(L, t__)) /* 'pos' part: restore 'p' */
/* macro to check stack size and GC */
#define checkstackGC(L,fsize) \
luaD_checkstackaux(L, (fsize), luaC_checkGC(L), (void)0)
/* type of protected functions, to be ran by 'runprotected' */
typedef void (*Pfunc) (lua_State *L, void *ud);
LUAI_FUNC void luaD_seterrorobj (lua_State *L, int errcode, StkId oldtop);
LUAI_FUNC int luaD_protectedparser (lua_State *L, ZIO *z, const char *name,
const char *mode);
LUAI_FUNC void luaD_hook (lua_State *L, int event, int line,
int fTransfer, int nTransfer);
LUAI_FUNC void luaD_hookcall (lua_State *L, CallInfo *ci);
LUAI_FUNC int luaD_pretailcall (lua_State *L, CallInfo *ci, StkId func,
int narg1, int delta);
LUAI_FUNC CallInfo *luaD_precall (lua_State *L, StkId func, int nResults);
LUAI_FUNC void luaD_call (lua_State *L, StkId func, int nResults);
LUAI_FUNC void luaD_callnoyield (lua_State *L, StkId func, int nResults);
LUAI_FUNC int luaD_closeprotected (lua_State *L, ptrdiff_t level, int status);
LUAI_FUNC int luaD_pcall (lua_State *L, Pfunc func, void *u,
ptrdiff_t oldtop, ptrdiff_t ef);
LUAI_FUNC void luaD_poscall (lua_State *L, CallInfo *ci, int nres);
LUAI_FUNC int luaD_reallocstack (lua_State *L, int newsize, int raiseerror);
LUAI_FUNC int luaD_growstack (lua_State *L, int n, int raiseerror);
LUAI_FUNC void luaD_shrinkstack (lua_State *L);
LUAI_FUNC void luaD_inctop (lua_State *L);
LUAI_FUNC l_noret luaD_throw (lua_State *L, int errcode);
LUAI_FUNC int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud);
#endif

View File

@@ -0,0 +1,230 @@
/*
** $Id: ldump.c $
** save precompiled Lua chunks
** See Copyright Notice in lua.h
*/
#define ldump_c
#define LUA_CORE
#include "lprefix.h"
#include <limits.h>
#include <stddef.h>
#include "lua.h"
#include "lobject.h"
#include "lstate.h"
#include "lundump.h"
typedef struct {
lua_State *L;
lua_Writer writer;
void *data;
int strip;
int status;
} DumpState;
/*
** All high-level dumps go through dumpVector; you can change it to
** change the endianness of the result
*/
#define dumpVector(D,v,n) dumpBlock(D,v,(n)*sizeof((v)[0]))
#define dumpLiteral(D, s) dumpBlock(D,s,sizeof(s) - sizeof(char))
static void dumpBlock (DumpState *D, const void *b, size_t size) {
if (D->status == 0 && size > 0) {
lua_unlock(D->L);
D->status = (*D->writer)(D->L, b, size, D->data);
lua_lock(D->L);
}
}
#define dumpVar(D,x) dumpVector(D,&x,1)
static void dumpByte (DumpState *D, int y) {
lu_byte x = (lu_byte)y;
dumpVar(D, x);
}
/*
** 'dumpSize' buffer size: each byte can store up to 7 bits. (The "+6"
** rounds up the division.)
*/
#define DIBS ((sizeof(size_t) * CHAR_BIT + 6) / 7)
static void dumpSize (DumpState *D, size_t x) {
lu_byte buff[DIBS];
int n = 0;
do {
buff[DIBS - (++n)] = x & 0x7f; /* fill buffer in reverse order */
x >>= 7;
} while (x != 0);
buff[DIBS - 1] |= 0x80; /* mark last byte */
dumpVector(D, buff + DIBS - n, n);
}
static void dumpInt (DumpState *D, int x) {
dumpSize(D, x);
}
static void dumpNumber (DumpState *D, lua_Number x) {
dumpVar(D, x);
}
static void dumpInteger (DumpState *D, lua_Integer x) {
dumpVar(D, x);
}
static void dumpString (DumpState *D, const TString *s) {
if (s == NULL)
dumpSize(D, 0);
else {
size_t size = tsslen(s);
const char *str = getstr(s);
dumpSize(D, size + 1);
dumpVector(D, str, size);
}
}
static void dumpCode (DumpState *D, const Proto *f) {
dumpInt(D, f->sizecode);
dumpVector(D, f->code, f->sizecode);
}
static void dumpFunction(DumpState *D, const Proto *f, TString *psource);
static void dumpConstants (DumpState *D, const Proto *f) {
int i;
int n = f->sizek;
dumpInt(D, n);
for (i = 0; i < n; i++) {
const TValue *o = &f->k[i];
int tt = ttypetag(o);
dumpByte(D, tt);
switch (tt) {
case LUA_VNUMFLT:
dumpNumber(D, fltvalue(o));
break;
case LUA_VNUMINT:
dumpInteger(D, ivalue(o));
break;
case LUA_VSHRSTR:
case LUA_VLNGSTR:
dumpString(D, tsvalue(o));
break;
default:
lua_assert(tt == LUA_VNIL || tt == LUA_VFALSE || tt == LUA_VTRUE);
}
}
}
static void dumpProtos (DumpState *D, const Proto *f) {
int i;
int n = f->sizep;
dumpInt(D, n);
for (i = 0; i < n; i++)
dumpFunction(D, f->p[i], f->source);
}
static void dumpUpvalues (DumpState *D, const Proto *f) {
int i, n = f->sizeupvalues;
dumpInt(D, n);
for (i = 0; i < n; i++) {
dumpByte(D, f->upvalues[i].instack);
dumpByte(D, f->upvalues[i].idx);
dumpByte(D, f->upvalues[i].kind);
}
}
static void dumpDebug (DumpState *D, const Proto *f) {
int i, n;
n = (D->strip) ? 0 : f->sizelineinfo;
dumpInt(D, n);
dumpVector(D, f->lineinfo, n);
n = (D->strip) ? 0 : f->sizeabslineinfo;
dumpInt(D, n);
for (i = 0; i < n; i++) {
dumpInt(D, f->abslineinfo[i].pc);
dumpInt(D, f->abslineinfo[i].line);
}
n = (D->strip) ? 0 : f->sizelocvars;
dumpInt(D, n);
for (i = 0; i < n; i++) {
dumpString(D, f->locvars[i].varname);
dumpInt(D, f->locvars[i].startpc);
dumpInt(D, f->locvars[i].endpc);
}
n = (D->strip) ? 0 : f->sizeupvalues;
dumpInt(D, n);
for (i = 0; i < n; i++)
dumpString(D, f->upvalues[i].name);
}
static void dumpFunction (DumpState *D, const Proto *f, TString *psource) {
if (D->strip || f->source == psource)
dumpString(D, NULL); /* no debug info or same source as its parent */
else
dumpString(D, f->source);
dumpInt(D, f->linedefined);
dumpInt(D, f->lastlinedefined);
dumpByte(D, f->numparams);
dumpByte(D, f->is_vararg);
dumpByte(D, f->maxstacksize);
dumpCode(D, f);
dumpConstants(D, f);
dumpUpvalues(D, f);
dumpProtos(D, f);
dumpDebug(D, f);
}
static void dumpHeader (DumpState *D) {
dumpLiteral(D, LUA_SIGNATURE);
dumpByte(D, LUAC_VERSION);
dumpByte(D, LUAC_FORMAT);
dumpLiteral(D, LUAC_DATA);
dumpByte(D, sizeof(Instruction));
dumpByte(D, sizeof(lua_Integer));
dumpByte(D, sizeof(lua_Number));
dumpInteger(D, LUAC_INT);
dumpNumber(D, LUAC_NUM);
}
/*
** dump Lua function as precompiled chunk
*/
int luaU_dump(lua_State *L, const Proto *f, lua_Writer w, void *data,
int strip) {
DumpState D;
D.L = L;
D.writer = w;
D.data = data;
D.strip = strip;
D.status = 0;
dumpHeader(&D);
dumpByte(&D, f->sizeupvalues);
dumpFunction(&D, f, NULL);
return D.status;
}

View File

@@ -0,0 +1,294 @@
/*
** $Id: lfunc.c $
** Auxiliary functions to manipulate prototypes and closures
** See Copyright Notice in lua.h
*/
#define lfunc_c
#define LUA_CORE
#include "lprefix.h"
#include <stddef.h>
#include "lua.h"
#include "ldebug.h"
#include "ldo.h"
#include "lfunc.h"
#include "lgc.h"
#include "lmem.h"
#include "lobject.h"
#include "lstate.h"
CClosure *luaF_newCclosure (lua_State *L, int nupvals) {
GCObject *o = luaC_newobj(L, LUA_VCCL, sizeCclosure(nupvals));
CClosure *c = gco2ccl(o);
c->nupvalues = cast_byte(nupvals);
return c;
}
LClosure *luaF_newLclosure (lua_State *L, int nupvals) {
GCObject *o = luaC_newobj(L, LUA_VLCL, sizeLclosure(nupvals));
LClosure *c = gco2lcl(o);
c->p = NULL;
c->nupvalues = cast_byte(nupvals);
while (nupvals--) c->upvals[nupvals] = NULL;
return c;
}
/*
** fill a closure with new closed upvalues
*/
void luaF_initupvals (lua_State *L, LClosure *cl) {
int i;
for (i = 0; i < cl->nupvalues; i++) {
GCObject *o = luaC_newobj(L, LUA_VUPVAL, sizeof(UpVal));
UpVal *uv = gco2upv(o);
uv->v.p = &uv->u.value; /* make it closed */
setnilvalue(uv->v.p);
cl->upvals[i] = uv;
luaC_objbarrier(L, cl, uv);
}
}
/*
** Create a new upvalue at the given level, and link it to the list of
** open upvalues of 'L' after entry 'prev'.
**/
static UpVal *newupval (lua_State *L, StkId level, UpVal **prev) {
GCObject *o = luaC_newobj(L, LUA_VUPVAL, sizeof(UpVal));
UpVal *uv = gco2upv(o);
UpVal *next = *prev;
uv->v.p = s2v(level); /* current value lives in the stack */
uv->u.open.next = next; /* link it to list of open upvalues */
uv->u.open.previous = prev;
if (next)
next->u.open.previous = &uv->u.open.next;
*prev = uv;
if (!isintwups(L)) { /* thread not in list of threads with upvalues? */
L->twups = G(L)->twups; /* link it to the list */
G(L)->twups = L;
}
return uv;
}
/*
** Find and reuse, or create if it does not exist, an upvalue
** at the given level.
*/
UpVal *luaF_findupval (lua_State *L, StkId level) {
UpVal **pp = &L->openupval;
UpVal *p;
lua_assert(isintwups(L) || L->openupval == NULL);
while ((p = *pp) != NULL && uplevel(p) >= level) { /* search for it */
lua_assert(!isdead(G(L), p));
if (uplevel(p) == level) /* corresponding upvalue? */
return p; /* return it */
pp = &p->u.open.next;
}
/* not found: create a new upvalue after 'pp' */
return newupval(L, level, pp);
}
/*
** Call closing method for object 'obj' with error message 'err'. The
** boolean 'yy' controls whether the call is yieldable.
** (This function assumes EXTRA_STACK.)
*/
static void callclosemethod (lua_State *L, TValue *obj, TValue *err, int yy) {
StkId top = L->top.p;
const TValue *tm = luaT_gettmbyobj(L, obj, TM_CLOSE);
setobj2s(L, top, tm); /* will call metamethod... */
setobj2s(L, top + 1, obj); /* with 'self' as the 1st argument */
setobj2s(L, top + 2, err); /* and error msg. as 2nd argument */
L->top.p = top + 3; /* add function and arguments */
if (yy)
luaD_call(L, top, 0);
else
luaD_callnoyield(L, top, 0);
}
/*
** Check whether object at given level has a close metamethod and raise
** an error if not.
*/
static void checkclosemth (lua_State *L, StkId level) {
const TValue *tm = luaT_gettmbyobj(L, s2v(level), TM_CLOSE);
if (ttisnil(tm)) { /* no metamethod? */
int idx = cast_int(level - L->ci->func.p); /* variable index */
const char *vname = luaG_findlocal(L, L->ci, idx, NULL);
if (vname == NULL) vname = "?";
luaG_runerror(L, "variable '%s' got a non-closable value", vname);
}
}
/*
** Prepare and call a closing method.
** If status is CLOSEKTOP, the call to the closing method will be pushed
** at the top of the stack. Otherwise, values can be pushed right after
** the 'level' of the upvalue being closed, as everything after that
** won't be used again.
*/
static void prepcallclosemth (lua_State *L, StkId level, int status, int yy) {
TValue *uv = s2v(level); /* value being closed */
TValue *errobj;
if (status == CLOSEKTOP)
errobj = &G(L)->nilvalue; /* error object is nil */
else { /* 'luaD_seterrorobj' will set top to level + 2 */
errobj = s2v(level + 1); /* error object goes after 'uv' */
luaD_seterrorobj(L, status, level + 1); /* set error object */
}
callclosemethod(L, uv, errobj, yy);
}
/*
** Maximum value for deltas in 'tbclist', dependent on the type
** of delta. (This macro assumes that an 'L' is in scope where it
** is used.)
*/
#define MAXDELTA \
((256ul << ((sizeof(L->stack.p->tbclist.delta) - 1) * 8)) - 1)
/*
** Insert a variable in the list of to-be-closed variables.
*/
void luaF_newtbcupval (lua_State *L, StkId level) {
lua_assert(level > L->tbclist.p);
if (l_isfalse(s2v(level)))
return; /* false doesn't need to be closed */
checkclosemth(L, level); /* value must have a close method */
while (cast_uint(level - L->tbclist.p) > MAXDELTA) {
L->tbclist.p += MAXDELTA; /* create a dummy node at maximum delta */
L->tbclist.p->tbclist.delta = 0;
}
level->tbclist.delta = cast(unsigned short, level - L->tbclist.p);
L->tbclist.p = level;
}
void luaF_unlinkupval (UpVal *uv) {
lua_assert(upisopen(uv));
*uv->u.open.previous = uv->u.open.next;
if (uv->u.open.next)
uv->u.open.next->u.open.previous = uv->u.open.previous;
}
/*
** Close all upvalues up to the given stack level.
*/
void luaF_closeupval (lua_State *L, StkId level) {
UpVal *uv;
StkId upl; /* stack index pointed by 'uv' */
while ((uv = L->openupval) != NULL && (upl = uplevel(uv)) >= level) {
TValue *slot = &uv->u.value; /* new position for value */
lua_assert(uplevel(uv) < L->top.p);
luaF_unlinkupval(uv); /* remove upvalue from 'openupval' list */
setobj(L, slot, uv->v.p); /* move value to upvalue slot */
uv->v.p = slot; /* now current value lives here */
if (!iswhite(uv)) { /* neither white nor dead? */
nw2black(uv); /* closed upvalues cannot be gray */
luaC_barrier(L, uv, slot);
}
}
}
/*
** Remove first element from the tbclist plus its dummy nodes.
*/
static void poptbclist (lua_State *L) {
StkId tbc = L->tbclist.p;
lua_assert(tbc->tbclist.delta > 0); /* first element cannot be dummy */
tbc -= tbc->tbclist.delta;
while (tbc > L->stack.p && tbc->tbclist.delta == 0)
tbc -= MAXDELTA; /* remove dummy nodes */
L->tbclist.p = tbc;
}
/*
** Close all upvalues and to-be-closed variables up to the given stack
** level. Return restored 'level'.
*/
StkId luaF_close (lua_State *L, StkId level, int status, int yy) {
ptrdiff_t levelrel = savestack(L, level);
luaF_closeupval(L, level); /* first, close the upvalues */
while (L->tbclist.p >= level) { /* traverse tbc's down to that level */
StkId tbc = L->tbclist.p; /* get variable index */
poptbclist(L); /* remove it from list */
prepcallclosemth(L, tbc, status, yy); /* close variable */
level = restorestack(L, levelrel);
}
return level;
}
Proto *luaF_newproto (lua_State *L) {
GCObject *o = luaC_newobj(L, LUA_VPROTO, sizeof(Proto));
Proto *f = gco2p(o);
f->k = NULL;
f->sizek = 0;
f->p = NULL;
f->sizep = 0;
f->code = NULL;
f->sizecode = 0;
f->lineinfo = NULL;
f->sizelineinfo = 0;
f->abslineinfo = NULL;
f->sizeabslineinfo = 0;
f->upvalues = NULL;
f->sizeupvalues = 0;
f->numparams = 0;
f->is_vararg = 0;
f->maxstacksize = 0;
f->locvars = NULL;
f->sizelocvars = 0;
f->linedefined = 0;
f->lastlinedefined = 0;
f->source = NULL;
return f;
}
void luaF_freeproto (lua_State *L, Proto *f) {
luaM_freearray(L, f->code, f->sizecode);
luaM_freearray(L, f->p, f->sizep);
luaM_freearray(L, f->k, f->sizek);
luaM_freearray(L, f->lineinfo, f->sizelineinfo);
luaM_freearray(L, f->abslineinfo, f->sizeabslineinfo);
luaM_freearray(L, f->locvars, f->sizelocvars);
luaM_freearray(L, f->upvalues, f->sizeupvalues);
luaM_free(L, f);
}
/*
** Look for n-th local variable at line 'line' in function 'func'.
** Returns NULL if not found.
*/
const char *luaF_getlocalname (const Proto *f, int local_number, int pc) {
int i;
for (i = 0; i<f->sizelocvars && f->locvars[i].startpc <= pc; i++) {
if (pc < f->locvars[i].endpc) { /* is variable active? */
local_number--;
if (local_number == 0)
return getstr(f->locvars[i].varname);
}
}
return NULL; /* not found */
}

View File

@@ -0,0 +1,64 @@
/*
** $Id: lfunc.h $
** Auxiliary functions to manipulate prototypes and closures
** See Copyright Notice in lua.h
*/
#ifndef lfunc_h
#define lfunc_h
#include "lobject.h"
#define sizeCclosure(n) (cast_int(offsetof(CClosure, upvalue)) + \
cast_int(sizeof(TValue)) * (n))
#define sizeLclosure(n) (cast_int(offsetof(LClosure, upvals)) + \
cast_int(sizeof(TValue *)) * (n))
/* test whether thread is in 'twups' list */
#define isintwups(L) (L->twups != L)
/*
** maximum number of upvalues in a closure (both C and Lua). (Value
** must fit in a VM register.)
*/
#define MAXUPVAL 255
#define upisopen(up) ((up)->v.p != &(up)->u.value)
#define uplevel(up) check_exp(upisopen(up), cast(StkId, (up)->v.p))
/*
** maximum number of misses before giving up the cache of closures
** in prototypes
*/
#define MAXMISS 10
/* special status to close upvalues preserving the top of the stack */
#define CLOSEKTOP (-1)
LUAI_FUNC Proto *luaF_newproto (lua_State *L);
LUAI_FUNC CClosure *luaF_newCclosure (lua_State *L, int nupvals);
LUAI_FUNC LClosure *luaF_newLclosure (lua_State *L, int nupvals);
LUAI_FUNC void luaF_initupvals (lua_State *L, LClosure *cl);
LUAI_FUNC UpVal *luaF_findupval (lua_State *L, StkId level);
LUAI_FUNC void luaF_newtbcupval (lua_State *L, StkId level);
LUAI_FUNC void luaF_closeupval (lua_State *L, StkId level);
LUAI_FUNC StkId luaF_close (lua_State *L, StkId level, int status, int yy);
LUAI_FUNC void luaF_unlinkupval (UpVal *uv);
LUAI_FUNC void luaF_freeproto (lua_State *L, Proto *f);
LUAI_FUNC const char *luaF_getlocalname (const Proto *func, int local_number,
int pc);
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,202 @@
/*
** $Id: lgc.h $
** Garbage Collector
** See Copyright Notice in lua.h
*/
#ifndef lgc_h
#define lgc_h
#include "lobject.h"
#include "lstate.h"
/*
** Collectable objects may have one of three colors: white, which means
** the object is not marked; gray, which means the object is marked, but
** its references may be not marked; and black, which means that the
** object and all its references are marked. The main invariant of the
** garbage collector, while marking objects, is that a black object can
** never point to a white one. Moreover, any gray object must be in a
** "gray list" (gray, grayagain, weak, allweak, ephemeron) so that it
** can be visited again before finishing the collection cycle. (Open
** upvalues are an exception to this rule.) These lists have no meaning
** when the invariant is not being enforced (e.g., sweep phase).
*/
/*
** Possible states of the Garbage Collector
*/
#define GCSpropagate 0
#define GCSenteratomic 1
#define GCSatomic 2
#define GCSswpallgc 3
#define GCSswpfinobj 4
#define GCSswptobefnz 5
#define GCSswpend 6
#define GCScallfin 7
#define GCSpause 8
#define issweepphase(g) \
(GCSswpallgc <= (g)->gcstate && (g)->gcstate <= GCSswpend)
/*
** macro to tell when main invariant (white objects cannot point to black
** ones) must be kept. During a collection, the sweep
** phase may break the invariant, as objects turned white may point to
** still-black objects. The invariant is restored when sweep ends and
** all objects are white again.
*/
#define keepinvariant(g) ((g)->gcstate <= GCSatomic)
/*
** some useful bit tricks
*/
#define resetbits(x,m) ((x) &= cast_byte(~(m)))
#define setbits(x,m) ((x) |= (m))
#define testbits(x,m) ((x) & (m))
#define bitmask(b) (1<<(b))
#define bit2mask(b1,b2) (bitmask(b1) | bitmask(b2))
#define l_setbit(x,b) setbits(x, bitmask(b))
#define resetbit(x,b) resetbits(x, bitmask(b))
#define testbit(x,b) testbits(x, bitmask(b))
/*
** Layout for bit use in 'marked' field. First three bits are
** used for object "age" in generational mode. Last bit is used
** by tests.
*/
#define WHITE0BIT 3 /* object is white (type 0) */
#define WHITE1BIT 4 /* object is white (type 1) */
#define BLACKBIT 5 /* object is black */
#define FINALIZEDBIT 6 /* object has been marked for finalization */
#define TESTBIT 7
#define WHITEBITS bit2mask(WHITE0BIT, WHITE1BIT)
#define iswhite(x) testbits((x)->marked, WHITEBITS)
#define isblack(x) testbit((x)->marked, BLACKBIT)
#define isgray(x) /* neither white nor black */ \
(!testbits((x)->marked, WHITEBITS | bitmask(BLACKBIT)))
#define tofinalize(x) testbit((x)->marked, FINALIZEDBIT)
#define otherwhite(g) ((g)->currentwhite ^ WHITEBITS)
#define isdeadm(ow,m) ((m) & (ow))
#define isdead(g,v) isdeadm(otherwhite(g), (v)->marked)
#define changewhite(x) ((x)->marked ^= WHITEBITS)
#define nw2black(x) \
check_exp(!iswhite(x), l_setbit((x)->marked, BLACKBIT))
#define luaC_white(g) cast_byte((g)->currentwhite & WHITEBITS)
/* object age in generational mode */
#define G_NEW 0 /* created in current cycle */
#define G_SURVIVAL 1 /* created in previous cycle */
#define G_OLD0 2 /* marked old by frw. barrier in this cycle */
#define G_OLD1 3 /* first full cycle as old */
#define G_OLD 4 /* really old object (not to be visited) */
#define G_TOUCHED1 5 /* old object touched this cycle */
#define G_TOUCHED2 6 /* old object touched in previous cycle */
#define AGEBITS 7 /* all age bits (111) */
#define getage(o) ((o)->marked & AGEBITS)
#define setage(o,a) ((o)->marked = cast_byte(((o)->marked & (~AGEBITS)) | a))
#define isold(o) (getage(o) > G_SURVIVAL)
#define changeage(o,f,t) \
check_exp(getage(o) == (f), (o)->marked ^= ((f)^(t)))
/* Default Values for GC parameters */
#define LUAI_GENMAJORMUL 100
#define LUAI_GENMINORMUL 20
/* wait memory to double before starting new cycle */
#define LUAI_GCPAUSE 200
/*
** some gc parameters are stored divided by 4 to allow a maximum value
** up to 1023 in a 'lu_byte'.
*/
#define getgcparam(p) ((p) * 4)
#define setgcparam(p,v) ((p) = (v) / 4)
#define LUAI_GCMUL 100
/* how much to allocate before next GC step (log2) */
#define LUAI_GCSTEPSIZE 13 /* 8 KB */
/*
** Check whether the declared GC mode is generational. While in
** generational mode, the collector can go temporarily to incremental
** mode to improve performance. This is signaled by 'g->lastatomic != 0'.
*/
#define isdecGCmodegen(g) (g->gckind == KGC_GEN || g->lastatomic != 0)
/*
** Control when GC is running:
*/
#define GCSTPUSR 1 /* bit true when GC stopped by user */
#define GCSTPGC 2 /* bit true when GC stopped by itself */
#define GCSTPCLS 4 /* bit true when closing Lua state */
#define gcrunning(g) ((g)->gcstp == 0)
/*
** Does one step of collection when debt becomes positive. 'pre'/'pos'
** allows some adjustments to be done only when needed. macro
** 'condchangemem' is used only for heavy tests (forcing a full
** GC cycle on every opportunity)
*/
#define luaC_condGC(L,pre,pos) \
{ if (G(L)->GCdebt > 0) { pre; luaC_step(L); pos;}; \
condchangemem(L,pre,pos); }
/* more often than not, 'pre'/'pos' are empty */
#define luaC_checkGC(L) luaC_condGC(L,(void)0,(void)0)
#define luaC_objbarrier(L,p,o) ( \
(isblack(p) && iswhite(o)) ? \
luaC_barrier_(L,obj2gco(p),obj2gco(o)) : cast_void(0))
#define luaC_barrier(L,p,v) ( \
iscollectable(v) ? luaC_objbarrier(L,p,gcvalue(v)) : cast_void(0))
#define luaC_objbarrierback(L,p,o) ( \
(isblack(p) && iswhite(o)) ? luaC_barrierback_(L,p) : cast_void(0))
#define luaC_barrierback(L,p,v) ( \
iscollectable(v) ? luaC_objbarrierback(L, p, gcvalue(v)) : cast_void(0))
LUAI_FUNC void luaC_fix (lua_State *L, GCObject *o);
LUAI_FUNC void luaC_freeallobjects (lua_State *L);
LUAI_FUNC void luaC_step (lua_State *L);
LUAI_FUNC void luaC_runtilstate (lua_State *L, int statesmask);
LUAI_FUNC void luaC_fullgc (lua_State *L, int isemergency);
LUAI_FUNC GCObject *luaC_newobj (lua_State *L, int tt, size_t sz);
LUAI_FUNC GCObject *luaC_newobjdt (lua_State *L, int tt, size_t sz,
size_t offset);
LUAI_FUNC void luaC_barrier_ (lua_State *L, GCObject *o, GCObject *v);
LUAI_FUNC void luaC_barrierback_ (lua_State *L, GCObject *o);
LUAI_FUNC void luaC_checkfinalizer (lua_State *L, GCObject *o, Table *mt);
LUAI_FUNC void luaC_changemode (lua_State *L, int newmode);
#endif

View File

@@ -0,0 +1,65 @@
/*
** $Id: linit.c $
** Initialization of libraries for lua.c and other clients
** See Copyright Notice in lua.h
*/
#define linit_c
#define LUA_LIB
/*
** If you embed Lua in your program and need to open the standard
** libraries, call luaL_openlibs in your program. If you need a
** different set of libraries, copy this file to your project and edit
** it to suit your needs.
**
** You can also *preload* libraries, so that a later 'require' can
** open the library, which is already linked to the application.
** For that, do the following code:
**
** luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE);
** lua_pushcfunction(L, luaopen_modname);
** lua_setfield(L, -2, modname);
** lua_pop(L, 1); // remove PRELOAD table
*/
#include "lprefix.h"
#include <stddef.h>
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
/*
** these libs are loaded by lua.c and are readily available to any Lua
** program
*/
static const luaL_Reg loadedlibs[] = {
{LUA_GNAME, luaopen_base},
{LUA_LOADLIBNAME, luaopen_package},
{LUA_COLIBNAME, luaopen_coroutine},
{LUA_TABLIBNAME, luaopen_table},
{LUA_IOLIBNAME, luaopen_io},
{LUA_OSLIBNAME, luaopen_os},
{LUA_STRLIBNAME, luaopen_string},
{LUA_MATHLIBNAME, luaopen_math},
{LUA_UTF8LIBNAME, luaopen_utf8},
{LUA_DBLIBNAME, luaopen_debug},
{NULL, NULL}
};
LUALIB_API void luaL_openlibs (lua_State *L) {
const luaL_Reg *lib;
/* "require" functions from 'loadedlibs' and set results to global table */
for (lib = loadedlibs; lib->func; lib++) {
luaL_requiref(L, lib->name, lib->func, 1);
lua_pop(L, 1); /* remove lib */
}
}

View File

@@ -0,0 +1,841 @@
/*
** $Id: liolib.c $
** Standard I/O (and system) library
** See Copyright Notice in lua.h
*/
#define liolib_c
#define LUA_LIB
#include "lprefix.h"
#include <ctype.h>
#include <errno.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
/*
** Change this macro to accept other modes for 'fopen' besides
** the standard ones.
*/
#if !defined(l_checkmode)
/* accepted extensions to 'mode' in 'fopen' */
#if !defined(L_MODEEXT)
#define L_MODEEXT "b"
#endif
/* Check whether 'mode' matches '[rwa]%+?[L_MODEEXT]*' */
static int l_checkmode (const char *mode) {
return (*mode != '\0' && strchr("rwa", *(mode++)) != NULL &&
(*mode != '+' || ((void)(++mode), 1)) && /* skip if char is '+' */
(strspn(mode, L_MODEEXT) == strlen(mode))); /* check extensions */
}
#endif
/*
** {======================================================
** l_popen spawns a new process connected to the current
** one through the file streams.
** =======================================================
*/
#if !defined(l_popen) /* { */
#if defined(LUA_USE_POSIX) /* { */
#define l_popen(L,c,m) (fflush(NULL), popen(c,m))
#define l_pclose(L,file) (pclose(file))
#elif defined(LUA_USE_WINDOWS) /* }{ */
#define l_popen(L,c,m) (_popen(c,m))
#define l_pclose(L,file) (_pclose(file))
#if !defined(l_checkmodep)
/* Windows accepts "[rw][bt]?" as valid modes */
#define l_checkmodep(m) ((m[0] == 'r' || m[0] == 'w') && \
(m[1] == '\0' || ((m[1] == 'b' || m[1] == 't') && m[2] == '\0')))
#endif
#else /* }{ */
/* ISO C definitions */
#define l_popen(L,c,m) \
((void)c, (void)m, \
luaL_error(L, "'popen' not supported"), \
(FILE*)0)
#define l_pclose(L,file) ((void)L, (void)file, -1)
#endif /* } */
#endif /* } */
#if !defined(l_checkmodep)
/* By default, Lua accepts only "r" or "w" as valid modes */
#define l_checkmodep(m) ((m[0] == 'r' || m[0] == 'w') && m[1] == '\0')
#endif
/* }====================================================== */
#if !defined(l_getc) /* { */
#if defined(LUA_USE_POSIX)
#define l_getc(f) getc_unlocked(f)
#define l_lockfile(f) flockfile(f)
#define l_unlockfile(f) funlockfile(f)
#else
#define l_getc(f) getc(f)
#define l_lockfile(f) ((void)0)
#define l_unlockfile(f) ((void)0)
#endif
#endif /* } */
/*
** {======================================================
** l_fseek: configuration for longer offsets
** =======================================================
*/
#if !defined(l_fseek) /* { */
#if defined(LUA_USE_POSIX) /* { */
#include <sys/types.h>
#define l_fseek(f,o,w) fseeko(f,o,w)
#define l_ftell(f) ftello(f)
#define l_seeknum off_t
#elif defined(LUA_USE_WINDOWS) && !defined(_CRTIMP_TYPEINFO) \
&& defined(_MSC_VER) && (_MSC_VER >= 1400) /* }{ */
/* Windows (but not DDK) and Visual C++ 2005 or higher */
#define l_fseek(f,o,w) _fseeki64(f,o,w)
#define l_ftell(f) _ftelli64(f)
#define l_seeknum __int64
#else /* }{ */
/* ISO C definitions */
#define l_fseek(f,o,w) fseek(f,o,w)
#define l_ftell(f) ftell(f)
#define l_seeknum long
#endif /* } */
#endif /* } */
/* }====================================================== */
#define IO_PREFIX "_IO_"
#define IOPREF_LEN (sizeof(IO_PREFIX)/sizeof(char) - 1)
#define IO_INPUT (IO_PREFIX "input")
#define IO_OUTPUT (IO_PREFIX "output")
typedef luaL_Stream LStream;
#define tolstream(L) ((LStream *)luaL_checkudata(L, 1, LUA_FILEHANDLE))
#define isclosed(p) ((p)->closef == NULL)
static int io_type (lua_State *L) {
LStream *p;
luaL_checkany(L, 1);
p = (LStream *)luaL_testudata(L, 1, LUA_FILEHANDLE);
if (p == NULL)
luaL_pushfail(L); /* not a file */
else if (isclosed(p))
lua_pushliteral(L, "closed file");
else
lua_pushliteral(L, "file");
return 1;
}
static int f_tostring (lua_State *L) {
LStream *p = tolstream(L);
if (isclosed(p))
lua_pushliteral(L, "file (closed)");
else
lua_pushfstring(L, "file (%p)", p->f);
return 1;
}
static FILE *tofile (lua_State *L) {
LStream *p = tolstream(L);
if (l_unlikely(isclosed(p)))
luaL_error(L, "attempt to use a closed file");
lua_assert(p->f);
return p->f;
}
/*
** When creating file handles, always creates a 'closed' file handle
** before opening the actual file; so, if there is a memory error, the
** handle is in a consistent state.
*/
static LStream *newprefile (lua_State *L) {
LStream *p = (LStream *)lua_newuserdatauv(L, sizeof(LStream), 0);
p->closef = NULL; /* mark file handle as 'closed' */
luaL_setmetatable(L, LUA_FILEHANDLE);
return p;
}
/*
** Calls the 'close' function from a file handle. The 'volatile' avoids
** a bug in some versions of the Clang compiler (e.g., clang 3.0 for
** 32 bits).
*/
static int aux_close (lua_State *L) {
LStream *p = tolstream(L);
volatile lua_CFunction cf = p->closef;
p->closef = NULL; /* mark stream as closed */
return (*cf)(L); /* close it */
}
static int f_close (lua_State *L) {
tofile(L); /* make sure argument is an open stream */
return aux_close(L);
}
static int io_close (lua_State *L) {
if (lua_isnone(L, 1)) /* no argument? */
lua_getfield(L, LUA_REGISTRYINDEX, IO_OUTPUT); /* use default output */
return f_close(L);
}
static int f_gc (lua_State *L) {
LStream *p = tolstream(L);
if (!isclosed(p) && p->f != NULL)
aux_close(L); /* ignore closed and incompletely open files */
return 0;
}
/*
** function to close regular files
*/
static int io_fclose (lua_State *L) {
LStream *p = tolstream(L);
errno = 0;
return luaL_fileresult(L, (fclose(p->f) == 0), NULL);
}
static LStream *newfile (lua_State *L) {
LStream *p = newprefile(L);
p->f = NULL;
p->closef = &io_fclose;
return p;
}
static void opencheck (lua_State *L, const char *fname, const char *mode) {
LStream *p = newfile(L);
p->f = fopen(fname, mode);
if (l_unlikely(p->f == NULL))
luaL_error(L, "cannot open file '%s' (%s)", fname, strerror(errno));
}
static int io_open (lua_State *L) {
const char *filename = luaL_checkstring(L, 1);
const char *mode = luaL_optstring(L, 2, "r");
LStream *p = newfile(L);
const char *md = mode; /* to traverse/check mode */
luaL_argcheck(L, l_checkmode(md), 2, "invalid mode");
errno = 0;
p->f = fopen(filename, mode);
return (p->f == NULL) ? luaL_fileresult(L, 0, filename) : 1;
}
/*
** function to close 'popen' files
*/
static int io_pclose (lua_State *L) {
LStream *p = tolstream(L);
errno = 0;
return luaL_execresult(L, l_pclose(L, p->f));
}
static int io_popen (lua_State *L) {
const char *filename = luaL_checkstring(L, 1);
const char *mode = luaL_optstring(L, 2, "r");
LStream *p = newprefile(L);
luaL_argcheck(L, l_checkmodep(mode), 2, "invalid mode");
errno = 0;
p->f = l_popen(L, filename, mode);
p->closef = &io_pclose;
return (p->f == NULL) ? luaL_fileresult(L, 0, filename) : 1;
}
static int io_tmpfile (lua_State *L) {
LStream *p = newfile(L);
errno = 0;
p->f = tmpfile();
return (p->f == NULL) ? luaL_fileresult(L, 0, NULL) : 1;
}
static FILE *getiofile (lua_State *L, const char *findex) {
LStream *p;
lua_getfield(L, LUA_REGISTRYINDEX, findex);
p = (LStream *)lua_touserdata(L, -1);
if (l_unlikely(isclosed(p)))
luaL_error(L, "default %s file is closed", findex + IOPREF_LEN);
return p->f;
}
static int g_iofile (lua_State *L, const char *f, const char *mode) {
if (!lua_isnoneornil(L, 1)) {
const char *filename = lua_tostring(L, 1);
if (filename)
opencheck(L, filename, mode);
else {
tofile(L); /* check that it's a valid file handle */
lua_pushvalue(L, 1);
}
lua_setfield(L, LUA_REGISTRYINDEX, f);
}
/* return current value */
lua_getfield(L, LUA_REGISTRYINDEX, f);
return 1;
}
static int io_input (lua_State *L) {
return g_iofile(L, IO_INPUT, "r");
}
static int io_output (lua_State *L) {
return g_iofile(L, IO_OUTPUT, "w");
}
static int io_readline (lua_State *L);
/*
** maximum number of arguments to 'f:lines'/'io.lines' (it + 3 must fit
** in the limit for upvalues of a closure)
*/
#define MAXARGLINE 250
/*
** Auxiliary function to create the iteration function for 'lines'.
** The iteration function is a closure over 'io_readline', with
** the following upvalues:
** 1) The file being read (first value in the stack)
** 2) the number of arguments to read
** 3) a boolean, true iff file has to be closed when finished ('toclose')
** *) a variable number of format arguments (rest of the stack)
*/
static void aux_lines (lua_State *L, int toclose) {
int n = lua_gettop(L) - 1; /* number of arguments to read */
luaL_argcheck(L, n <= MAXARGLINE, MAXARGLINE + 2, "too many arguments");
lua_pushvalue(L, 1); /* file */
lua_pushinteger(L, n); /* number of arguments to read */
lua_pushboolean(L, toclose); /* close/not close file when finished */
lua_rotate(L, 2, 3); /* move the three values to their positions */
lua_pushcclosure(L, io_readline, 3 + n);
}
static int f_lines (lua_State *L) {
tofile(L); /* check that it's a valid file handle */
aux_lines(L, 0);
return 1;
}
/*
** Return an iteration function for 'io.lines'. If file has to be
** closed, also returns the file itself as a second result (to be
** closed as the state at the exit of a generic for).
*/
static int io_lines (lua_State *L) {
int toclose;
if (lua_isnone(L, 1)) lua_pushnil(L); /* at least one argument */
if (lua_isnil(L, 1)) { /* no file name? */
lua_getfield(L, LUA_REGISTRYINDEX, IO_INPUT); /* get default input */
lua_replace(L, 1); /* put it at index 1 */
tofile(L); /* check that it's a valid file handle */
toclose = 0; /* do not close it after iteration */
}
else { /* open a new file */
const char *filename = luaL_checkstring(L, 1);
opencheck(L, filename, "r");
lua_replace(L, 1); /* put file at index 1 */
toclose = 1; /* close it after iteration */
}
aux_lines(L, toclose); /* push iteration function */
if (toclose) {
lua_pushnil(L); /* state */
lua_pushnil(L); /* control */
lua_pushvalue(L, 1); /* file is the to-be-closed variable (4th result) */
return 4;
}
else
return 1;
}
/*
** {======================================================
** READ
** =======================================================
*/
/* maximum length of a numeral */
#if !defined (L_MAXLENNUM)
#define L_MAXLENNUM 200
#endif
/* auxiliary structure used by 'read_number' */
typedef struct {
FILE *f; /* file being read */
int c; /* current character (look ahead) */
int n; /* number of elements in buffer 'buff' */
char buff[L_MAXLENNUM + 1]; /* +1 for ending '\0' */
} RN;
/*
** Add current char to buffer (if not out of space) and read next one
*/
static int nextc (RN *rn) {
if (l_unlikely(rn->n >= L_MAXLENNUM)) { /* buffer overflow? */
rn->buff[0] = '\0'; /* invalidate result */
return 0; /* fail */
}
else {
rn->buff[rn->n++] = rn->c; /* save current char */
rn->c = l_getc(rn->f); /* read next one */
return 1;
}
}
/*
** Accept current char if it is in 'set' (of size 2)
*/
static int test2 (RN *rn, const char *set) {
if (rn->c == set[0] || rn->c == set[1])
return nextc(rn);
else return 0;
}
/*
** Read a sequence of (hex)digits
*/
static int readdigits (RN *rn, int hex) {
int count = 0;
while ((hex ? isxdigit(rn->c) : isdigit(rn->c)) && nextc(rn))
count++;
return count;
}
/*
** Read a number: first reads a valid prefix of a numeral into a buffer.
** Then it calls 'lua_stringtonumber' to check whether the format is
** correct and to convert it to a Lua number.
*/
static int read_number (lua_State *L, FILE *f) {
RN rn;
int count = 0;
int hex = 0;
char decp[2];
rn.f = f; rn.n = 0;
decp[0] = lua_getlocaledecpoint(); /* get decimal point from locale */
decp[1] = '.'; /* always accept a dot */
l_lockfile(rn.f);
do { rn.c = l_getc(rn.f); } while (isspace(rn.c)); /* skip spaces */
test2(&rn, "-+"); /* optional sign */
if (test2(&rn, "00")) {
if (test2(&rn, "xX")) hex = 1; /* numeral is hexadecimal */
else count = 1; /* count initial '0' as a valid digit */
}
count += readdigits(&rn, hex); /* integral part */
if (test2(&rn, decp)) /* decimal point? */
count += readdigits(&rn, hex); /* fractional part */
if (count > 0 && test2(&rn, (hex ? "pP" : "eE"))) { /* exponent mark? */
test2(&rn, "-+"); /* exponent sign */
readdigits(&rn, 0); /* exponent digits */
}
ungetc(rn.c, rn.f); /* unread look-ahead char */
l_unlockfile(rn.f);
rn.buff[rn.n] = '\0'; /* finish string */
if (l_likely(lua_stringtonumber(L, rn.buff)))
return 1; /* ok, it is a valid number */
else { /* invalid format */
lua_pushnil(L); /* "result" to be removed */
return 0; /* read fails */
}
}
static int test_eof (lua_State *L, FILE *f) {
int c = getc(f);
ungetc(c, f); /* no-op when c == EOF */
lua_pushliteral(L, "");
return (c != EOF);
}
static int read_line (lua_State *L, FILE *f, int chop) {
luaL_Buffer b;
int c;
luaL_buffinit(L, &b);
do { /* may need to read several chunks to get whole line */
char *buff = luaL_prepbuffer(&b); /* preallocate buffer space */
int i = 0;
l_lockfile(f); /* no memory errors can happen inside the lock */
while (i < LUAL_BUFFERSIZE && (c = l_getc(f)) != EOF && c != '\n')
buff[i++] = c; /* read up to end of line or buffer limit */
l_unlockfile(f);
luaL_addsize(&b, i);
} while (c != EOF && c != '\n'); /* repeat until end of line */
if (!chop && c == '\n') /* want a newline and have one? */
luaL_addchar(&b, c); /* add ending newline to result */
luaL_pushresult(&b); /* close buffer */
/* return ok if read something (either a newline or something else) */
return (c == '\n' || lua_rawlen(L, -1) > 0);
}
static void read_all (lua_State *L, FILE *f) {
size_t nr;
luaL_Buffer b;
luaL_buffinit(L, &b);
do { /* read file in chunks of LUAL_BUFFERSIZE bytes */
char *p = luaL_prepbuffer(&b);
nr = fread(p, sizeof(char), LUAL_BUFFERSIZE, f);
luaL_addsize(&b, nr);
} while (nr == LUAL_BUFFERSIZE);
luaL_pushresult(&b); /* close buffer */
}
static int read_chars (lua_State *L, FILE *f, size_t n) {
size_t nr; /* number of chars actually read */
char *p;
luaL_Buffer b;
luaL_buffinit(L, &b);
p = luaL_prepbuffsize(&b, n); /* prepare buffer to read whole block */
nr = fread(p, sizeof(char), n, f); /* try to read 'n' chars */
luaL_addsize(&b, nr);
luaL_pushresult(&b); /* close buffer */
return (nr > 0); /* true iff read something */
}
static int g_read (lua_State *L, FILE *f, int first) {
int nargs = lua_gettop(L) - 1;
int n, success;
clearerr(f);
errno = 0;
if (nargs == 0) { /* no arguments? */
success = read_line(L, f, 1);
n = first + 1; /* to return 1 result */
}
else {
/* ensure stack space for all results and for auxlib's buffer */
luaL_checkstack(L, nargs+LUA_MINSTACK, "too many arguments");
success = 1;
for (n = first; nargs-- && success; n++) {
if (lua_type(L, n) == LUA_TNUMBER) {
size_t l = (size_t)luaL_checkinteger(L, n);
success = (l == 0) ? test_eof(L, f) : read_chars(L, f, l);
}
else {
const char *p = luaL_checkstring(L, n);
if (*p == '*') p++; /* skip optional '*' (for compatibility) */
switch (*p) {
case 'n': /* number */
success = read_number(L, f);
break;
case 'l': /* line */
success = read_line(L, f, 1);
break;
case 'L': /* line with end-of-line */
success = read_line(L, f, 0);
break;
case 'a': /* file */
read_all(L, f); /* read entire file */
success = 1; /* always success */
break;
default:
return luaL_argerror(L, n, "invalid format");
}
}
}
}
if (ferror(f))
return luaL_fileresult(L, 0, NULL);
if (!success) {
lua_pop(L, 1); /* remove last result */
luaL_pushfail(L); /* push nil instead */
}
return n - first;
}
static int io_read (lua_State *L) {
return g_read(L, getiofile(L, IO_INPUT), 1);
}
static int f_read (lua_State *L) {
return g_read(L, tofile(L), 2);
}
/*
** Iteration function for 'lines'.
*/
static int io_readline (lua_State *L) {
LStream *p = (LStream *)lua_touserdata(L, lua_upvalueindex(1));
int i;
int n = (int)lua_tointeger(L, lua_upvalueindex(2));
if (isclosed(p)) /* file is already closed? */
return luaL_error(L, "file is already closed");
lua_settop(L , 1);
luaL_checkstack(L, n, "too many arguments");
for (i = 1; i <= n; i++) /* push arguments to 'g_read' */
lua_pushvalue(L, lua_upvalueindex(3 + i));
n = g_read(L, p->f, 2); /* 'n' is number of results */
lua_assert(n > 0); /* should return at least a nil */
if (lua_toboolean(L, -n)) /* read at least one value? */
return n; /* return them */
else { /* first result is false: EOF or error */
if (n > 1) { /* is there error information? */
/* 2nd result is error message */
return luaL_error(L, "%s", lua_tostring(L, -n + 1));
}
if (lua_toboolean(L, lua_upvalueindex(3))) { /* generator created file? */
lua_settop(L, 0); /* clear stack */
lua_pushvalue(L, lua_upvalueindex(1)); /* push file at index 1 */
aux_close(L); /* close it */
}
return 0;
}
}
/* }====================================================== */
static int g_write (lua_State *L, FILE *f, int arg) {
int nargs = lua_gettop(L) - arg;
int status = 1;
errno = 0;
for (; nargs--; arg++) {
if (lua_type(L, arg) == LUA_TNUMBER) {
/* optimization: could be done exactly as for strings */
int len = lua_isinteger(L, arg)
? fprintf(f, LUA_INTEGER_FMT,
(LUAI_UACINT)lua_tointeger(L, arg))
: fprintf(f, LUA_NUMBER_FMT,
(LUAI_UACNUMBER)lua_tonumber(L, arg));
status = status && (len > 0);
}
else {
size_t l;
const char *s = luaL_checklstring(L, arg, &l);
status = status && (fwrite(s, sizeof(char), l, f) == l);
}
}
if (l_likely(status))
return 1; /* file handle already on stack top */
else
return luaL_fileresult(L, status, NULL);
}
static int io_write (lua_State *L) {
return g_write(L, getiofile(L, IO_OUTPUT), 1);
}
static int f_write (lua_State *L) {
FILE *f = tofile(L);
lua_pushvalue(L, 1); /* push file at the stack top (to be returned) */
return g_write(L, f, 2);
}
static int f_seek (lua_State *L) {
static const int mode[] = {SEEK_SET, SEEK_CUR, SEEK_END};
static const char *const modenames[] = {"set", "cur", "end", NULL};
FILE *f = tofile(L);
int op = luaL_checkoption(L, 2, "cur", modenames);
lua_Integer p3 = luaL_optinteger(L, 3, 0);
l_seeknum offset = (l_seeknum)p3;
luaL_argcheck(L, (lua_Integer)offset == p3, 3,
"not an integer in proper range");
errno = 0;
op = l_fseek(f, offset, mode[op]);
if (l_unlikely(op))
return luaL_fileresult(L, 0, NULL); /* error */
else {
lua_pushinteger(L, (lua_Integer)l_ftell(f));
return 1;
}
}
static int f_setvbuf (lua_State *L) {
static const int mode[] = {_IONBF, _IOFBF, _IOLBF};
static const char *const modenames[] = {"no", "full", "line", NULL};
FILE *f = tofile(L);
int op = luaL_checkoption(L, 2, NULL, modenames);
lua_Integer sz = luaL_optinteger(L, 3, LUAL_BUFFERSIZE);
int res;
errno = 0;
res = setvbuf(f, NULL, mode[op], (size_t)sz);
return luaL_fileresult(L, res == 0, NULL);
}
static int io_flush (lua_State *L) {
FILE *f = getiofile(L, IO_OUTPUT);
errno = 0;
return luaL_fileresult(L, fflush(f) == 0, NULL);
}
static int f_flush (lua_State *L) {
FILE *f = tofile(L);
errno = 0;
return luaL_fileresult(L, fflush(f) == 0, NULL);
}
/*
** functions for 'io' library
*/
static const luaL_Reg iolib[] = {
{"close", io_close},
{"flush", io_flush},
{"input", io_input},
{"lines", io_lines},
{"open", io_open},
{"output", io_output},
{"popen", io_popen},
{"read", io_read},
{"tmpfile", io_tmpfile},
{"type", io_type},
{"write", io_write},
{NULL, NULL}
};
/*
** methods for file handles
*/
static const luaL_Reg meth[] = {
{"read", f_read},
{"write", f_write},
{"lines", f_lines},
{"flush", f_flush},
{"seek", f_seek},
{"close", f_close},
{"setvbuf", f_setvbuf},
{NULL, NULL}
};
/*
** metamethods for file handles
*/
static const luaL_Reg metameth[] = {
{"__index", NULL}, /* placeholder */
{"__gc", f_gc},
{"__close", f_gc},
{"__tostring", f_tostring},
{NULL, NULL}
};
static void createmeta (lua_State *L) {
luaL_newmetatable(L, LUA_FILEHANDLE); /* metatable for file handles */
luaL_setfuncs(L, metameth, 0); /* add metamethods to new metatable */
luaL_newlibtable(L, meth); /* create method table */
luaL_setfuncs(L, meth, 0); /* add file methods to method table */
lua_setfield(L, -2, "__index"); /* metatable.__index = method table */
lua_pop(L, 1); /* pop metatable */
}
/*
** function to (not) close the standard files stdin, stdout, and stderr
*/
static int io_noclose (lua_State *L) {
LStream *p = tolstream(L);
p->closef = &io_noclose; /* keep file opened */
luaL_pushfail(L);
lua_pushliteral(L, "cannot close standard file");
return 2;
}
static void createstdfile (lua_State *L, FILE *f, const char *k,
const char *fname) {
LStream *p = newprefile(L);
p->f = f;
p->closef = &io_noclose;
if (k != NULL) {
lua_pushvalue(L, -1);
lua_setfield(L, LUA_REGISTRYINDEX, k); /* add file to registry */
}
lua_setfield(L, -2, fname); /* add file to module */
}
LUAMOD_API int luaopen_io (lua_State *L) {
luaL_newlib(L, iolib); /* new module */
createmeta(L);
/* create (and set) default files */
createstdfile(L, stdin, IO_INPUT, "stdin");
createstdfile(L, stdout, IO_OUTPUT, "stdout");
createstdfile(L, stderr, NULL, "stderr");
return 1;
}

View File

@@ -0,0 +1,112 @@
/*
** $Id: ljumptab.h $
** Jump Table for the Lua interpreter
** See Copyright Notice in lua.h
*/
#undef vmdispatch
#undef vmcase
#undef vmbreak
#define vmdispatch(x) goto *disptab[x];
#define vmcase(l) L_##l:
#define vmbreak vmfetch(); vmdispatch(GET_OPCODE(i));
static const void *const disptab[NUM_OPCODES] = {
#if 0
** you can update the following list with this command:
**
** sed -n '/^OP_/\!d; s/OP_/\&\&L_OP_/ ; s/,.*/,/ ; s/\/.*// ; p' lopcodes.h
**
#endif
&&L_OP_MOVE,
&&L_OP_LOADI,
&&L_OP_LOADF,
&&L_OP_LOADK,
&&L_OP_LOADKX,
&&L_OP_LOADFALSE,
&&L_OP_LFALSESKIP,
&&L_OP_LOADTRUE,
&&L_OP_LOADNIL,
&&L_OP_GETUPVAL,
&&L_OP_SETUPVAL,
&&L_OP_GETTABUP,
&&L_OP_GETTABLE,
&&L_OP_GETI,
&&L_OP_GETFIELD,
&&L_OP_SETTABUP,
&&L_OP_SETTABLE,
&&L_OP_SETI,
&&L_OP_SETFIELD,
&&L_OP_NEWTABLE,
&&L_OP_SELF,
&&L_OP_ADDI,
&&L_OP_ADDK,
&&L_OP_SUBK,
&&L_OP_MULK,
&&L_OP_MODK,
&&L_OP_POWK,
&&L_OP_DIVK,
&&L_OP_IDIVK,
&&L_OP_BANDK,
&&L_OP_BORK,
&&L_OP_BXORK,
&&L_OP_SHRI,
&&L_OP_SHLI,
&&L_OP_ADD,
&&L_OP_SUB,
&&L_OP_MUL,
&&L_OP_MOD,
&&L_OP_POW,
&&L_OP_DIV,
&&L_OP_IDIV,
&&L_OP_BAND,
&&L_OP_BOR,
&&L_OP_BXOR,
&&L_OP_SHL,
&&L_OP_SHR,
&&L_OP_MMBIN,
&&L_OP_MMBINI,
&&L_OP_MMBINK,
&&L_OP_UNM,
&&L_OP_BNOT,
&&L_OP_NOT,
&&L_OP_LEN,
&&L_OP_CONCAT,
&&L_OP_CLOSE,
&&L_OP_TBC,
&&L_OP_JMP,
&&L_OP_EQ,
&&L_OP_LT,
&&L_OP_LE,
&&L_OP_EQK,
&&L_OP_EQI,
&&L_OP_LTI,
&&L_OP_LEI,
&&L_OP_GTI,
&&L_OP_GEI,
&&L_OP_TEST,
&&L_OP_TESTSET,
&&L_OP_CALL,
&&L_OP_TAILCALL,
&&L_OP_RETURN,
&&L_OP_RETURN0,
&&L_OP_RETURN1,
&&L_OP_FORLOOP,
&&L_OP_FORPREP,
&&L_OP_TFORPREP,
&&L_OP_TFORCALL,
&&L_OP_TFORLOOP,
&&L_OP_SETLIST,
&&L_OP_CLOSURE,
&&L_OP_VARARG,
&&L_OP_VARARGPREP,
&&L_OP_EXTRAARG
};

View File

@@ -0,0 +1,581 @@
/*
** $Id: llex.c $
** Lexical Analyzer
** See Copyright Notice in lua.h
*/
#define llex_c
#define LUA_CORE
#include "lprefix.h"
#include <locale.h>
#include <string.h>
#include "lua.h"
#include "lctype.h"
#include "ldebug.h"
#include "ldo.h"
#include "lgc.h"
#include "llex.h"
#include "lobject.h"
#include "lparser.h"
#include "lstate.h"
#include "lstring.h"
#include "ltable.h"
#include "lzio.h"
#define next(ls) (ls->current = zgetc(ls->z))
#define currIsNewline(ls) (ls->current == '\n' || ls->current == '\r')
/* ORDER RESERVED */
static const char *const luaX_tokens [] = {
"and", "break", "do", "else", "elseif",
"end", "false", "for", "function", "goto", "if",
"in", "local", "nil", "not", "or", "repeat",
"return", "then", "true", "until", "while",
"//", "..", "...", "==", ">=", "<=", "~=",
"<<", ">>", "::", "<eof>",
"<number>", "<integer>", "<name>", "<string>"
};
#define save_and_next(ls) (save(ls, ls->current), next(ls))
static l_noret lexerror (LexState *ls, const char *msg, int token);
static void save (LexState *ls, int c) {
Mbuffer *b = ls->buff;
if (luaZ_bufflen(b) + 1 > luaZ_sizebuffer(b)) {
size_t newsize;
if (luaZ_sizebuffer(b) >= MAX_SIZE/2)
lexerror(ls, "lexical element too long", 0);
newsize = luaZ_sizebuffer(b) * 2;
luaZ_resizebuffer(ls->L, b, newsize);
}
b->buffer[luaZ_bufflen(b)++] = cast_char(c);
}
void luaX_init (lua_State *L) {
int i;
TString *e = luaS_newliteral(L, LUA_ENV); /* create env name */
luaC_fix(L, obj2gco(e)); /* never collect this name */
for (i=0; i<NUM_RESERVED; i++) {
TString *ts = luaS_new(L, luaX_tokens[i]);
luaC_fix(L, obj2gco(ts)); /* reserved words are never collected */
ts->extra = cast_byte(i+1); /* reserved word */
}
}
const char *luaX_token2str (LexState *ls, int token) {
if (token < FIRST_RESERVED) { /* single-byte symbols? */
if (lisprint(token))
return luaO_pushfstring(ls->L, "'%c'", token);
else /* control character */
return luaO_pushfstring(ls->L, "'<\\%d>'", token);
}
else {
const char *s = luaX_tokens[token - FIRST_RESERVED];
if (token < TK_EOS) /* fixed format (symbols and reserved words)? */
return luaO_pushfstring(ls->L, "'%s'", s);
else /* names, strings, and numerals */
return s;
}
}
static const char *txtToken (LexState *ls, int token) {
switch (token) {
case TK_NAME: case TK_STRING:
case TK_FLT: case TK_INT:
save(ls, '\0');
return luaO_pushfstring(ls->L, "'%s'", luaZ_buffer(ls->buff));
default:
return luaX_token2str(ls, token);
}
}
static l_noret lexerror (LexState *ls, const char *msg, int token) {
msg = luaG_addinfo(ls->L, msg, ls->source, ls->linenumber);
if (token)
luaO_pushfstring(ls->L, "%s near %s", msg, txtToken(ls, token));
luaD_throw(ls->L, LUA_ERRSYNTAX);
}
l_noret luaX_syntaxerror (LexState *ls, const char *msg) {
lexerror(ls, msg, ls->t.token);
}
/*
** Creates a new string and anchors it in scanner's table so that it
** will not be collected until the end of the compilation; by that time
** it should be anchored somewhere. It also internalizes long strings,
** ensuring there is only one copy of each unique string. The table
** here is used as a set: the string enters as the key, while its value
** is irrelevant. We use the string itself as the value only because it
** is a TValue readily available. Later, the code generation can change
** this value.
*/
TString *luaX_newstring (LexState *ls, const char *str, size_t l) {
lua_State *L = ls->L;
TString *ts = luaS_newlstr(L, str, l); /* create new string */
const TValue *o = luaH_getstr(ls->h, ts);
if (!ttisnil(o)) /* string already present? */
ts = keystrval(nodefromval(o)); /* get saved copy */
else { /* not in use yet */
TValue *stv = s2v(L->top.p++); /* reserve stack space for string */
setsvalue(L, stv, ts); /* temporarily anchor the string */
luaH_finishset(L, ls->h, stv, o, stv); /* t[string] = string */
/* table is not a metatable, so it does not need to invalidate cache */
luaC_checkGC(L);
L->top.p--; /* remove string from stack */
}
return ts;
}
/*
** increment line number and skips newline sequence (any of
** \n, \r, \n\r, or \r\n)
*/
static void inclinenumber (LexState *ls) {
int old = ls->current;
lua_assert(currIsNewline(ls));
next(ls); /* skip '\n' or '\r' */
if (currIsNewline(ls) && ls->current != old)
next(ls); /* skip '\n\r' or '\r\n' */
if (++ls->linenumber >= MAX_INT)
lexerror(ls, "chunk has too many lines", 0);
}
void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source,
int firstchar) {
ls->t.token = 0;
ls->L = L;
ls->current = firstchar;
ls->lookahead.token = TK_EOS; /* no look-ahead token */
ls->z = z;
ls->fs = NULL;
ls->linenumber = 1;
ls->lastline = 1;
ls->source = source;
ls->envn = luaS_newliteral(L, LUA_ENV); /* get env name */
luaZ_resizebuffer(ls->L, ls->buff, LUA_MINBUFFER); /* initialize buffer */
}
/*
** =======================================================
** LEXICAL ANALYZER
** =======================================================
*/
static int check_next1 (LexState *ls, int c) {
if (ls->current == c) {
next(ls);
return 1;
}
else return 0;
}
/*
** Check whether current char is in set 'set' (with two chars) and
** saves it
*/
static int check_next2 (LexState *ls, const char *set) {
lua_assert(set[2] == '\0');
if (ls->current == set[0] || ls->current == set[1]) {
save_and_next(ls);
return 1;
}
else return 0;
}
/* LUA_NUMBER */
/*
** This function is quite liberal in what it accepts, as 'luaO_str2num'
** will reject ill-formed numerals. Roughly, it accepts the following
** pattern:
**
** %d(%x|%.|([Ee][+-]?))* | 0[Xx](%x|%.|([Pp][+-]?))*
**
** The only tricky part is to accept [+-] only after a valid exponent
** mark, to avoid reading '3-4' or '0xe+1' as a single number.
**
** The caller might have already read an initial dot.
*/
static int read_numeral (LexState *ls, SemInfo *seminfo) {
TValue obj;
const char *expo = "Ee";
int first = ls->current;
lua_assert(lisdigit(ls->current));
save_and_next(ls);
if (first == '0' && check_next2(ls, "xX")) /* hexadecimal? */
expo = "Pp";
for (;;) {
if (check_next2(ls, expo)) /* exponent mark? */
check_next2(ls, "-+"); /* optional exponent sign */
else if (lisxdigit(ls->current) || ls->current == '.') /* '%x|%.' */
save_and_next(ls);
else break;
}
if (lislalpha(ls->current)) /* is numeral touching a letter? */
save_and_next(ls); /* force an error */
save(ls, '\0');
if (luaO_str2num(luaZ_buffer(ls->buff), &obj) == 0) /* format error? */
lexerror(ls, "malformed number", TK_FLT);
if (ttisinteger(&obj)) {
seminfo->i = ivalue(&obj);
return TK_INT;
}
else {
lua_assert(ttisfloat(&obj));
seminfo->r = fltvalue(&obj);
return TK_FLT;
}
}
/*
** read a sequence '[=*[' or ']=*]', leaving the last bracket. If
** sequence is well formed, return its number of '='s + 2; otherwise,
** return 1 if it is a single bracket (no '='s and no 2nd bracket);
** otherwise (an unfinished '[==...') return 0.
*/
static size_t skip_sep (LexState *ls) {
size_t count = 0;
int s = ls->current;
lua_assert(s == '[' || s == ']');
save_and_next(ls);
while (ls->current == '=') {
save_and_next(ls);
count++;
}
return (ls->current == s) ? count + 2
: (count == 0) ? 1
: 0;
}
static void read_long_string (LexState *ls, SemInfo *seminfo, size_t sep) {
int line = ls->linenumber; /* initial line (for error message) */
save_and_next(ls); /* skip 2nd '[' */
if (currIsNewline(ls)) /* string starts with a newline? */
inclinenumber(ls); /* skip it */
for (;;) {
switch (ls->current) {
case EOZ: { /* error */
const char *what = (seminfo ? "string" : "comment");
const char *msg = luaO_pushfstring(ls->L,
"unfinished long %s (starting at line %d)", what, line);
lexerror(ls, msg, TK_EOS);
break; /* to avoid warnings */
}
case ']': {
if (skip_sep(ls) == sep) {
save_and_next(ls); /* skip 2nd ']' */
goto endloop;
}
break;
}
case '\n': case '\r': {
save(ls, '\n');
inclinenumber(ls);
if (!seminfo) luaZ_resetbuffer(ls->buff); /* avoid wasting space */
break;
}
default: {
if (seminfo) save_and_next(ls);
else next(ls);
}
}
} endloop:
if (seminfo)
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + sep,
luaZ_bufflen(ls->buff) - 2 * sep);
}
static void esccheck (LexState *ls, int c, const char *msg) {
if (!c) {
if (ls->current != EOZ)
save_and_next(ls); /* add current to buffer for error message */
lexerror(ls, msg, TK_STRING);
}
}
static int gethexa (LexState *ls) {
save_and_next(ls);
esccheck (ls, lisxdigit(ls->current), "hexadecimal digit expected");
return luaO_hexavalue(ls->current);
}
static int readhexaesc (LexState *ls) {
int r = gethexa(ls);
r = (r << 4) + gethexa(ls);
luaZ_buffremove(ls->buff, 2); /* remove saved chars from buffer */
return r;
}
static unsigned long readutf8esc (LexState *ls) {
unsigned long r;
int i = 4; /* chars to be removed: '\', 'u', '{', and first digit */
save_and_next(ls); /* skip 'u' */
esccheck(ls, ls->current == '{', "missing '{'");
r = gethexa(ls); /* must have at least one digit */
while (cast_void(save_and_next(ls)), lisxdigit(ls->current)) {
i++;
esccheck(ls, r <= (0x7FFFFFFFu >> 4), "UTF-8 value too large");
r = (r << 4) + luaO_hexavalue(ls->current);
}
esccheck(ls, ls->current == '}', "missing '}'");
next(ls); /* skip '}' */
luaZ_buffremove(ls->buff, i); /* remove saved chars from buffer */
return r;
}
static void utf8esc (LexState *ls) {
char buff[UTF8BUFFSZ];
int n = luaO_utf8esc(buff, readutf8esc(ls));
for (; n > 0; n--) /* add 'buff' to string */
save(ls, buff[UTF8BUFFSZ - n]);
}
static int readdecesc (LexState *ls) {
int i;
int r = 0; /* result accumulator */
for (i = 0; i < 3 && lisdigit(ls->current); i++) { /* read up to 3 digits */
r = 10*r + ls->current - '0';
save_and_next(ls);
}
esccheck(ls, r <= UCHAR_MAX, "decimal escape too large");
luaZ_buffremove(ls->buff, i); /* remove read digits from buffer */
return r;
}
static void read_string (LexState *ls, int del, SemInfo *seminfo) {
save_and_next(ls); /* keep delimiter (for error messages) */
while (ls->current != del) {
switch (ls->current) {
case EOZ:
lexerror(ls, "unfinished string", TK_EOS);
break; /* to avoid warnings */
case '\n':
case '\r':
lexerror(ls, "unfinished string", TK_STRING);
break; /* to avoid warnings */
case '\\': { /* escape sequences */
int c; /* final character to be saved */
save_and_next(ls); /* keep '\\' for error messages */
switch (ls->current) {
case 'a': c = '\a'; goto read_save;
case 'b': c = '\b'; goto read_save;
case 'f': c = '\f'; goto read_save;
case 'n': c = '\n'; goto read_save;
case 'r': c = '\r'; goto read_save;
case 't': c = '\t'; goto read_save;
case 'v': c = '\v'; goto read_save;
case 'x': c = readhexaesc(ls); goto read_save;
case 'u': utf8esc(ls); goto no_save;
case '\n': case '\r':
inclinenumber(ls); c = '\n'; goto only_save;
case '\\': case '\"': case '\'':
c = ls->current; goto read_save;
case EOZ: goto no_save; /* will raise an error next loop */
case 'z': { /* zap following span of spaces */
luaZ_buffremove(ls->buff, 1); /* remove '\\' */
next(ls); /* skip the 'z' */
while (lisspace(ls->current)) {
if (currIsNewline(ls)) inclinenumber(ls);
else next(ls);
}
goto no_save;
}
default: {
esccheck(ls, lisdigit(ls->current), "invalid escape sequence");
c = readdecesc(ls); /* digital escape '\ddd' */
goto only_save;
}
}
read_save:
next(ls);
/* go through */
only_save:
luaZ_buffremove(ls->buff, 1); /* remove '\\' */
save(ls, c);
/* go through */
no_save: break;
}
default:
save_and_next(ls);
}
}
save_and_next(ls); /* skip delimiter */
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + 1,
luaZ_bufflen(ls->buff) - 2);
}
static int llex (LexState *ls, SemInfo *seminfo) {
luaZ_resetbuffer(ls->buff);
for (;;) {
switch (ls->current) {
case '\n': case '\r': { /* line breaks */
inclinenumber(ls);
break;
}
case ' ': case '\f': case '\t': case '\v': { /* spaces */
next(ls);
break;
}
case '-': { /* '-' or '--' (comment) */
next(ls);
if (ls->current != '-') return '-';
/* else is a comment */
next(ls);
if (ls->current == '[') { /* long comment? */
size_t sep = skip_sep(ls);
luaZ_resetbuffer(ls->buff); /* 'skip_sep' may dirty the buffer */
if (sep >= 2) {
read_long_string(ls, NULL, sep); /* skip long comment */
luaZ_resetbuffer(ls->buff); /* previous call may dirty the buff. */
break;
}
}
/* else short comment */
while (!currIsNewline(ls) && ls->current != EOZ)
next(ls); /* skip until end of line (or end of file) */
break;
}
case '[': { /* long string or simply '[' */
size_t sep = skip_sep(ls);
if (sep >= 2) {
read_long_string(ls, seminfo, sep);
return TK_STRING;
}
else if (sep == 0) /* '[=...' missing second bracket? */
lexerror(ls, "invalid long string delimiter", TK_STRING);
return '[';
}
case '=': {
next(ls);
if (check_next1(ls, '=')) return TK_EQ; /* '==' */
else return '=';
}
case '<': {
next(ls);
if (check_next1(ls, '=')) return TK_LE; /* '<=' */
else if (check_next1(ls, '<')) return TK_SHL; /* '<<' */
else return '<';
}
case '>': {
next(ls);
if (check_next1(ls, '=')) return TK_GE; /* '>=' */
else if (check_next1(ls, '>')) return TK_SHR; /* '>>' */
else return '>';
}
case '/': {
next(ls);
if (check_next1(ls, '/')) return TK_IDIV; /* '//' */
else return '/';
}
case '~': {
next(ls);
if (check_next1(ls, '=')) return TK_NE; /* '~=' */
else return '~';
}
case ':': {
next(ls);
if (check_next1(ls, ':')) return TK_DBCOLON; /* '::' */
else return ':';
}
case '"': case '\'': { /* short literal strings */
read_string(ls, ls->current, seminfo);
return TK_STRING;
}
case '.': { /* '.', '..', '...', or number */
save_and_next(ls);
if (check_next1(ls, '.')) {
if (check_next1(ls, '.'))
return TK_DOTS; /* '...' */
else return TK_CONCAT; /* '..' */
}
else if (!lisdigit(ls->current)) return '.';
else return read_numeral(ls, seminfo);
}
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9': {
return read_numeral(ls, seminfo);
}
case EOZ: {
return TK_EOS;
}
default: {
if (lislalpha(ls->current)) { /* identifier or reserved word? */
TString *ts;
do {
save_and_next(ls);
} while (lislalnum(ls->current));
ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff));
seminfo->ts = ts;
if (isreserved(ts)) /* reserved word? */
return ts->extra - 1 + FIRST_RESERVED;
else {
return TK_NAME;
}
}
else { /* single-char tokens ('+', '*', '%', '{', '}', ...) */
int c = ls->current;
next(ls);
return c;
}
}
}
}
}
void luaX_next (LexState *ls) {
ls->lastline = ls->linenumber;
if (ls->lookahead.token != TK_EOS) { /* is there a look-ahead token? */
ls->t = ls->lookahead; /* use this one */
ls->lookahead.token = TK_EOS; /* and discharge it */
}
else
ls->t.token = llex(ls, &ls->t.seminfo); /* read next token */
}
int luaX_lookahead (LexState *ls) {
lua_assert(ls->lookahead.token == TK_EOS);
ls->lookahead.token = llex(ls, &ls->lookahead.seminfo);
return ls->lookahead.token;
}

View File

@@ -0,0 +1,91 @@
/*
** $Id: llex.h $
** Lexical Analyzer
** See Copyright Notice in lua.h
*/
#ifndef llex_h
#define llex_h
#include <limits.h>
#include "lobject.h"
#include "lzio.h"
/*
** Single-char tokens (terminal symbols) are represented by their own
** numeric code. Other tokens start at the following value.
*/
#define FIRST_RESERVED (UCHAR_MAX + 1)
#if !defined(LUA_ENV)
#define LUA_ENV "_ENV"
#endif
/*
* WARNING: if you change the order of this enumeration,
* grep "ORDER RESERVED"
*/
enum RESERVED {
/* terminal symbols denoted by reserved words */
TK_AND = FIRST_RESERVED, TK_BREAK,
TK_DO, TK_ELSE, TK_ELSEIF, TK_END, TK_FALSE, TK_FOR, TK_FUNCTION,
TK_GOTO, TK_IF, TK_IN, TK_LOCAL, TK_NIL, TK_NOT, TK_OR, TK_REPEAT,
TK_RETURN, TK_THEN, TK_TRUE, TK_UNTIL, TK_WHILE,
/* other terminal symbols */
TK_IDIV, TK_CONCAT, TK_DOTS, TK_EQ, TK_GE, TK_LE, TK_NE,
TK_SHL, TK_SHR,
TK_DBCOLON, TK_EOS,
TK_FLT, TK_INT, TK_NAME, TK_STRING
};
/* number of reserved words */
#define NUM_RESERVED (cast_int(TK_WHILE-FIRST_RESERVED + 1))
typedef union {
lua_Number r;
lua_Integer i;
TString *ts;
} SemInfo; /* semantics information */
typedef struct Token {
int token;
SemInfo seminfo;
} Token;
/* state of the lexer plus state of the parser when shared by all
functions */
typedef struct LexState {
int current; /* current character (charint) */
int linenumber; /* input line counter */
int lastline; /* line of last token 'consumed' */
Token t; /* current token */
Token lookahead; /* look ahead token */
struct FuncState *fs; /* current function (parser) */
struct lua_State *L;
ZIO *z; /* input stream */
Mbuffer *buff; /* buffer for tokens */
Table *h; /* to avoid collection/reuse strings */
struct Dyndata *dyd; /* dynamic structures used by the parser */
TString *source; /* current source name */
TString *envn; /* environment variable name */
} LexState;
LUAI_FUNC void luaX_init (lua_State *L);
LUAI_FUNC void luaX_setinput (lua_State *L, LexState *ls, ZIO *z,
TString *source, int firstchar);
LUAI_FUNC TString *luaX_newstring (LexState *ls, const char *str, size_t l);
LUAI_FUNC void luaX_next (LexState *ls);
LUAI_FUNC int luaX_lookahead (LexState *ls);
LUAI_FUNC l_noret luaX_syntaxerror (LexState *ls, const char *s);
LUAI_FUNC const char *luaX_token2str (LexState *ls, int token);
#endif

View File

@@ -0,0 +1,380 @@
/*
** $Id: llimits.h $
** Limits, basic types, and some other 'installation-dependent' definitions
** See Copyright Notice in lua.h
*/
#ifndef llimits_h
#define llimits_h
#include <limits.h>
#include <stddef.h>
#include "lua.h"
/*
** 'lu_mem' and 'l_mem' are unsigned/signed integers big enough to count
** the total memory used by Lua (in bytes). Usually, 'size_t' and
** 'ptrdiff_t' should work, but we use 'long' for 16-bit machines.
*/
#if defined(LUAI_MEM) /* { external definitions? */
typedef LUAI_UMEM lu_mem;
typedef LUAI_MEM l_mem;
#elif LUAI_IS32INT /* }{ */
typedef size_t lu_mem;
typedef ptrdiff_t l_mem;
#else /* 16-bit ints */ /* }{ */
typedef unsigned long lu_mem;
typedef long l_mem;
#endif /* } */
/* chars used as small naturals (so that 'char' is reserved for characters) */
typedef unsigned char lu_byte;
typedef signed char ls_byte;
/* maximum value for size_t */
#define MAX_SIZET ((size_t)(~(size_t)0))
/* maximum size visible for Lua (must be representable in a lua_Integer) */
#define MAX_SIZE (sizeof(size_t) < sizeof(lua_Integer) ? MAX_SIZET \
: (size_t)(LUA_MAXINTEGER))
#define MAX_LUMEM ((lu_mem)(~(lu_mem)0))
#define MAX_LMEM ((l_mem)(MAX_LUMEM >> 1))
#define MAX_INT INT_MAX /* maximum value of an int */
/*
** floor of the log2 of the maximum signed value for integral type 't'.
** (That is, maximum 'n' such that '2^n' fits in the given signed type.)
*/
#define log2maxs(t) (sizeof(t) * 8 - 2)
/*
** test whether an unsigned value is a power of 2 (or zero)
*/
#define ispow2(x) (((x) & ((x) - 1)) == 0)
/* number of chars of a literal string without the ending \0 */
#define LL(x) (sizeof(x)/sizeof(char) - 1)
/*
** conversion of pointer to unsigned integer: this is for hashing only;
** there is no problem if the integer cannot hold the whole pointer
** value. (In strict ISO C this may cause undefined behavior, but no
** actual machine seems to bother.)
*/
#if !defined(LUA_USE_C89) && defined(__STDC_VERSION__) && \
__STDC_VERSION__ >= 199901L
#include <stdint.h>
#if defined(UINTPTR_MAX) /* even in C99 this type is optional */
#define L_P2I uintptr_t
#else /* no 'intptr'? */
#define L_P2I uintmax_t /* use the largest available integer */
#endif
#else /* C89 option */
#define L_P2I size_t
#endif
#define point2uint(p) ((unsigned int)((L_P2I)(p) & UINT_MAX))
/* types of 'usual argument conversions' for lua_Number and lua_Integer */
typedef LUAI_UACNUMBER l_uacNumber;
typedef LUAI_UACINT l_uacInt;
/*
** Internal assertions for in-house debugging
*/
#if defined LUAI_ASSERT
#undef NDEBUG
#include <assert.h>
#define lua_assert(c) assert(c)
#endif
#if defined(lua_assert)
#define check_exp(c,e) (lua_assert(c), (e))
/* to avoid problems with conditions too long */
#define lua_longassert(c) ((c) ? (void)0 : lua_assert(0))
#else
#define lua_assert(c) ((void)0)
#define check_exp(c,e) (e)
#define lua_longassert(c) ((void)0)
#endif
/*
** assertion for checking API calls
*/
#if !defined(luai_apicheck)
#define luai_apicheck(l,e) ((void)l, lua_assert(e))
#endif
#define api_check(l,e,msg) luai_apicheck(l,(e) && msg)
/* macro to avoid warnings about unused variables */
#if !defined(UNUSED)
#define UNUSED(x) ((void)(x))
#endif
/* type casts (a macro highlights casts in the code) */
#define cast(t, exp) ((t)(exp))
#define cast_void(i) cast(void, (i))
#define cast_voidp(i) cast(void *, (i))
#define cast_num(i) cast(lua_Number, (i))
#define cast_int(i) cast(int, (i))
#define cast_uint(i) cast(unsigned int, (i))
#define cast_byte(i) cast(lu_byte, (i))
#define cast_uchar(i) cast(unsigned char, (i))
#define cast_char(i) cast(char, (i))
#define cast_charp(i) cast(char *, (i))
#define cast_sizet(i) cast(size_t, (i))
/* cast a signed lua_Integer to lua_Unsigned */
#if !defined(l_castS2U)
#define l_castS2U(i) ((lua_Unsigned)(i))
#endif
/*
** cast a lua_Unsigned to a signed lua_Integer; this cast is
** not strict ISO C, but two-complement architectures should
** work fine.
*/
#if !defined(l_castU2S)
#define l_castU2S(i) ((lua_Integer)(i))
#endif
/*
** non-return type
*/
#if !defined(l_noret)
#if defined(__GNUC__)
#define l_noret void __attribute__((noreturn))
#elif defined(_MSC_VER) && _MSC_VER >= 1200
#define l_noret void __declspec(noreturn)
#else
#define l_noret void
#endif
#endif
/*
** Inline functions
*/
#if !defined(LUA_USE_C89)
#define l_inline inline
#elif defined(__GNUC__)
#define l_inline __inline__
#else
#define l_inline /* empty */
#endif
#define l_sinline static l_inline
/*
** type for virtual-machine instructions;
** must be an unsigned with (at least) 4 bytes (see details in lopcodes.h)
*/
#if LUAI_IS32INT
typedef unsigned int l_uint32;
#else
typedef unsigned long l_uint32;
#endif
typedef l_uint32 Instruction;
/*
** Maximum length for short strings, that is, strings that are
** internalized. (Cannot be smaller than reserved words or tags for
** metamethods, as these strings must be internalized;
** #("function") = 8, #("__newindex") = 10.)
*/
#if !defined(LUAI_MAXSHORTLEN)
#define LUAI_MAXSHORTLEN 40
#endif
/*
** Initial size for the string table (must be power of 2).
** The Lua core alone registers ~50 strings (reserved words +
** metaevent keys + a few others). Libraries would typically add
** a few dozens more.
*/
#if !defined(MINSTRTABSIZE)
#define MINSTRTABSIZE 128
#endif
/*
** Size of cache for strings in the API. 'N' is the number of
** sets (better be a prime) and "M" is the size of each set (M == 1
** makes a direct cache.)
*/
#if !defined(STRCACHE_N)
#define STRCACHE_N 53
#define STRCACHE_M 2
#endif
/* minimum size for string buffer */
#if !defined(LUA_MINBUFFER)
#define LUA_MINBUFFER 32
#endif
/*
** Maximum depth for nested C calls, syntactical nested non-terminals,
** and other features implemented through recursion in C. (Value must
** fit in a 16-bit unsigned integer. It must also be compatible with
** the size of the C stack.)
*/
#if !defined(LUAI_MAXCCALLS)
#define LUAI_MAXCCALLS 200
#endif
/*
** macros that are executed whenever program enters the Lua core
** ('lua_lock') and leaves the core ('lua_unlock')
*/
#if !defined(lua_lock)
#define lua_lock(L) ((void) 0)
#define lua_unlock(L) ((void) 0)
#endif
/*
** macro executed during Lua functions at points where the
** function can yield.
*/
#if !defined(luai_threadyield)
#define luai_threadyield(L) {lua_unlock(L); lua_lock(L);}
#endif
/*
** these macros allow user-specific actions when a thread is
** created/deleted/resumed/yielded.
*/
#if !defined(luai_userstateopen)
#define luai_userstateopen(L) ((void)L)
#endif
#if !defined(luai_userstateclose)
#define luai_userstateclose(L) ((void)L)
#endif
#if !defined(luai_userstatethread)
#define luai_userstatethread(L,L1) ((void)L)
#endif
#if !defined(luai_userstatefree)
#define luai_userstatefree(L,L1) ((void)L)
#endif
#if !defined(luai_userstateresume)
#define luai_userstateresume(L,n) ((void)L)
#endif
#if !defined(luai_userstateyield)
#define luai_userstateyield(L,n) ((void)L)
#endif
/*
** The luai_num* macros define the primitive operations over numbers.
*/
/* floor division (defined as 'floor(a/b)') */
#if !defined(luai_numidiv)
#define luai_numidiv(L,a,b) ((void)L, l_floor(luai_numdiv(L,a,b)))
#endif
/* float division */
#if !defined(luai_numdiv)
#define luai_numdiv(L,a,b) ((a)/(b))
#endif
/*
** modulo: defined as 'a - floor(a/b)*b'; the direct computation
** using this definition has several problems with rounding errors,
** so it is better to use 'fmod'. 'fmod' gives the result of
** 'a - trunc(a/b)*b', and therefore must be corrected when
** 'trunc(a/b) ~= floor(a/b)'. That happens when the division has a
** non-integer negative result: non-integer result is equivalent to
** a non-zero remainder 'm'; negative result is equivalent to 'a' and
** 'b' with different signs, or 'm' and 'b' with different signs
** (as the result 'm' of 'fmod' has the same sign of 'a').
*/
#if !defined(luai_nummod)
#define luai_nummod(L,a,b,m) \
{ (void)L; (m) = l_mathop(fmod)(a,b); \
if (((m) > 0) ? (b) < 0 : ((m) < 0 && (b) > 0)) (m) += (b); }
#endif
/* exponentiation */
#if !defined(luai_numpow)
#define luai_numpow(L,a,b) \
((void)L, (b == 2) ? (a)*(a) : l_mathop(pow)(a,b))
#endif
/* the others are quite standard operations */
#if !defined(luai_numadd)
#define luai_numadd(L,a,b) ((a)+(b))
#define luai_numsub(L,a,b) ((a)-(b))
#define luai_nummul(L,a,b) ((a)*(b))
#define luai_numunm(L,a) (-(a))
#define luai_numeq(a,b) ((a)==(b))
#define luai_numlt(a,b) ((a)<(b))
#define luai_numle(a,b) ((a)<=(b))
#define luai_numgt(a,b) ((a)>(b))
#define luai_numge(a,b) ((a)>=(b))
#define luai_numisnan(a) (!luai_numeq((a), (a)))
#endif
/*
** macro to control inclusion of some hard tests on stack reallocation
*/
#if !defined(HARDSTACKTESTS)
#define condmovestack(L,pre,pos) ((void)0)
#else
/* realloc stack keeping its size */
#define condmovestack(L,pre,pos) \
{ int sz_ = stacksize(L); pre; luaD_reallocstack((L), sz_, 0); pos; }
#endif
#if !defined(HARDMEMTESTS)
#define condchangemem(L,pre,pos) ((void)0)
#else
#define condchangemem(L,pre,pos) \
{ if (gcrunning(G(L))) { pre; luaC_fullgc(L, 0); pos; } }
#endif
#endif

View File

@@ -0,0 +1,781 @@
/*
** $Id: lmathlib.c $
** Standard mathematical library
** See Copyright Notice in lua.h
*/
#define lmathlib_c
#define LUA_LIB
#include "lprefix.h"
#include <float.h>
#include <limits.h>
#include <math.h>
#include <stdlib.h>
#include <time.h>
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
#undef PI
#define PI (l_mathop(3.141592653589793238462643383279502884))
static int math_abs (lua_State *L) {
if (lua_isinteger(L, 1)) {
lua_Integer n = lua_tointeger(L, 1);
if (n < 0) n = (lua_Integer)(0u - (lua_Unsigned)n);
lua_pushinteger(L, n);
}
else
lua_pushnumber(L, l_mathop(fabs)(luaL_checknumber(L, 1)));
return 1;
}
static int math_sin (lua_State *L) {
lua_pushnumber(L, l_mathop(sin)(luaL_checknumber(L, 1)));
return 1;
}
static int math_cos (lua_State *L) {
lua_pushnumber(L, l_mathop(cos)(luaL_checknumber(L, 1)));
return 1;
}
static int math_tan (lua_State *L) {
lua_pushnumber(L, l_mathop(tan)(luaL_checknumber(L, 1)));
return 1;
}
static int math_asin (lua_State *L) {
lua_pushnumber(L, l_mathop(asin)(luaL_checknumber(L, 1)));
return 1;
}
static int math_acos (lua_State *L) {
lua_pushnumber(L, l_mathop(acos)(luaL_checknumber(L, 1)));
return 1;
}
static int math_atan (lua_State *L) {
lua_Number y = luaL_checknumber(L, 1);
lua_Number x = luaL_optnumber(L, 2, 1);
lua_pushnumber(L, l_mathop(atan2)(y, x));
return 1;
}
static int math_toint (lua_State *L) {
int valid;
lua_Integer n = lua_tointegerx(L, 1, &valid);
if (l_likely(valid))
lua_pushinteger(L, n);
else {
luaL_checkany(L, 1);
luaL_pushfail(L); /* value is not convertible to integer */
}
return 1;
}
static void pushnumint (lua_State *L, lua_Number d) {
lua_Integer n;
if (lua_numbertointeger(d, &n)) /* does 'd' fit in an integer? */
lua_pushinteger(L, n); /* result is integer */
else
lua_pushnumber(L, d); /* result is float */
}
static int math_floor (lua_State *L) {
if (lua_isinteger(L, 1))
lua_settop(L, 1); /* integer is its own floor */
else {
lua_Number d = l_mathop(floor)(luaL_checknumber(L, 1));
pushnumint(L, d);
}
return 1;
}
static int math_ceil (lua_State *L) {
if (lua_isinteger(L, 1))
lua_settop(L, 1); /* integer is its own ceil */
else {
lua_Number d = l_mathop(ceil)(luaL_checknumber(L, 1));
pushnumint(L, d);
}
return 1;
}
static int math_fmod (lua_State *L) {
if (lua_isinteger(L, 1) && lua_isinteger(L, 2)) {
lua_Integer d = lua_tointeger(L, 2);
if ((lua_Unsigned)d + 1u <= 1u) { /* special cases: -1 or 0 */
luaL_argcheck(L, d != 0, 2, "zero");
lua_pushinteger(L, 0); /* avoid overflow with 0x80000... / -1 */
}
else
lua_pushinteger(L, lua_tointeger(L, 1) % d);
}
else
lua_pushnumber(L, l_mathop(fmod)(luaL_checknumber(L, 1),
luaL_checknumber(L, 2)));
return 1;
}
/*
** next function does not use 'modf', avoiding problems with 'double*'
** (which is not compatible with 'float*') when lua_Number is not
** 'double'.
*/
static int math_modf (lua_State *L) {
if (lua_isinteger(L ,1)) {
lua_settop(L, 1); /* number is its own integer part */
lua_pushnumber(L, 0); /* no fractional part */
}
else {
lua_Number n = luaL_checknumber(L, 1);
/* integer part (rounds toward zero) */
lua_Number ip = (n < 0) ? l_mathop(ceil)(n) : l_mathop(floor)(n);
pushnumint(L, ip);
/* fractional part (test needed for inf/-inf) */
lua_pushnumber(L, (n == ip) ? l_mathop(0.0) : (n - ip));
}
return 2;
}
static int math_sqrt (lua_State *L) {
lua_pushnumber(L, l_mathop(sqrt)(luaL_checknumber(L, 1)));
return 1;
}
static int math_ult (lua_State *L) {
lua_Integer a = luaL_checkinteger(L, 1);
lua_Integer b = luaL_checkinteger(L, 2);
lua_pushboolean(L, (lua_Unsigned)a < (lua_Unsigned)b);
return 1;
}
static int math_log (lua_State *L) {
lua_Number x = luaL_checknumber(L, 1);
lua_Number res;
if (lua_isnoneornil(L, 2))
res = l_mathop(log)(x);
else {
lua_Number base = luaL_checknumber(L, 2);
#if !defined(LUA_USE_C89)
if (base == l_mathop(2.0))
res = l_mathop(log2)(x);
else
#endif
if (base == l_mathop(10.0))
res = l_mathop(log10)(x);
else
res = l_mathop(log)(x)/l_mathop(log)(base);
}
lua_pushnumber(L, res);
return 1;
}
static int math_exp (lua_State *L) {
lua_pushnumber(L, l_mathop(exp)(luaL_checknumber(L, 1)));
return 1;
}
static int math_deg (lua_State *L) {
lua_pushnumber(L, luaL_checknumber(L, 1) * (l_mathop(180.0) / PI));
return 1;
}
static int math_rad (lua_State *L) {
lua_pushnumber(L, luaL_checknumber(L, 1) * (PI / l_mathop(180.0)));
return 1;
}
static int math_min (lua_State *L) {
int n = lua_gettop(L); /* number of arguments */
int imin = 1; /* index of current minimum value */
int i;
luaL_argcheck(L, n >= 1, 1, "value expected");
for (i = 2; i <= n; i++) {
if (lua_compare(L, i, imin, LUA_OPLT))
imin = i;
}
lua_pushvalue(L, imin);
return 1;
}
static int math_max (lua_State *L) {
int n = lua_gettop(L); /* number of arguments */
int imax = 1; /* index of current maximum value */
int i;
luaL_argcheck(L, n >= 1, 1, "value expected");
for (i = 2; i <= n; i++) {
if (lua_compare(L, imax, i, LUA_OPLT))
imax = i;
}
lua_pushvalue(L, imax);
return 1;
}
static int math_type (lua_State *L) {
if (lua_type(L, 1) == LUA_TNUMBER)
lua_pushstring(L, (lua_isinteger(L, 1)) ? "integer" : "float");
else {
luaL_checkany(L, 1);
luaL_pushfail(L);
}
return 1;
}
/*
** {==================================================================
** Pseudo-Random Number Generator based on 'xoshiro256**'.
** ===================================================================
*/
/*
** This code uses lots of shifts. ANSI C does not allow shifts greater
** than or equal to the width of the type being shifted, so some shifts
** are written in convoluted ways to match that restriction. For
** preprocessor tests, it assumes a width of 32 bits, so the maximum
** shift there is 31 bits.
*/
/* number of binary digits in the mantissa of a float */
#define FIGS l_floatatt(MANT_DIG)
#if FIGS > 64
/* there are only 64 random bits; use them all */
#undef FIGS
#define FIGS 64
#endif
/*
** LUA_RAND32 forces the use of 32-bit integers in the implementation
** of the PRN generator (mainly for testing).
*/
#if !defined(LUA_RAND32) && !defined(Rand64)
/* try to find an integer type with at least 64 bits */
#if ((ULONG_MAX >> 31) >> 31) >= 3
/* 'long' has at least 64 bits */
#define Rand64 unsigned long
#define SRand64 long
#elif !defined(LUA_USE_C89) && defined(LLONG_MAX)
/* there is a 'long long' type (which must have at least 64 bits) */
#define Rand64 unsigned long long
#define SRand64 long long
#elif ((LUA_MAXUNSIGNED >> 31) >> 31) >= 3
/* 'lua_Unsigned' has at least 64 bits */
#define Rand64 lua_Unsigned
#define SRand64 lua_Integer
#endif
#endif
#if defined(Rand64) /* { */
/*
** Standard implementation, using 64-bit integers.
** If 'Rand64' has more than 64 bits, the extra bits do not interfere
** with the 64 initial bits, except in a right shift. Moreover, the
** final result has to discard the extra bits.
*/
/* avoid using extra bits when needed */
#define trim64(x) ((x) & 0xffffffffffffffffu)
/* rotate left 'x' by 'n' bits */
static Rand64 rotl (Rand64 x, int n) {
return (x << n) | (trim64(x) >> (64 - n));
}
static Rand64 nextrand (Rand64 *state) {
Rand64 state0 = state[0];
Rand64 state1 = state[1];
Rand64 state2 = state[2] ^ state0;
Rand64 state3 = state[3] ^ state1;
Rand64 res = rotl(state1 * 5, 7) * 9;
state[0] = state0 ^ state3;
state[1] = state1 ^ state2;
state[2] = state2 ^ (state1 << 17);
state[3] = rotl(state3, 45);
return res;
}
/*
** Convert bits from a random integer into a float in the
** interval [0,1), getting the higher FIG bits from the
** random unsigned integer and converting that to a float.
** Some old Microsoft compilers cannot cast an unsigned long
** to a floating-point number, so we use a signed long as an
** intermediary. When lua_Number is float or double, the shift ensures
** that 'sx' is non negative; in that case, a good compiler will remove
** the correction.
*/
/* must throw out the extra (64 - FIGS) bits */
#define shift64_FIG (64 - FIGS)
/* 2^(-FIGS) == 2^-1 / 2^(FIGS-1) */
#define scaleFIG (l_mathop(0.5) / ((Rand64)1 << (FIGS - 1)))
static lua_Number I2d (Rand64 x) {
SRand64 sx = (SRand64)(trim64(x) >> shift64_FIG);
lua_Number res = (lua_Number)(sx) * scaleFIG;
if (sx < 0)
res += l_mathop(1.0); /* correct the two's complement if negative */
lua_assert(0 <= res && res < 1);
return res;
}
/* convert a 'Rand64' to a 'lua_Unsigned' */
#define I2UInt(x) ((lua_Unsigned)trim64(x))
/* convert a 'lua_Unsigned' to a 'Rand64' */
#define Int2I(x) ((Rand64)(x))
#else /* no 'Rand64' }{ */
/* get an integer with at least 32 bits */
#if LUAI_IS32INT
typedef unsigned int lu_int32;
#else
typedef unsigned long lu_int32;
#endif
/*
** Use two 32-bit integers to represent a 64-bit quantity.
*/
typedef struct Rand64 {
lu_int32 h; /* higher half */
lu_int32 l; /* lower half */
} Rand64;
/*
** If 'lu_int32' has more than 32 bits, the extra bits do not interfere
** with the 32 initial bits, except in a right shift and comparisons.
** Moreover, the final result has to discard the extra bits.
*/
/* avoid using extra bits when needed */
#define trim32(x) ((x) & 0xffffffffu)
/*
** basic operations on 'Rand64' values
*/
/* build a new Rand64 value */
static Rand64 packI (lu_int32 h, lu_int32 l) {
Rand64 result;
result.h = h;
result.l = l;
return result;
}
/* return i << n */
static Rand64 Ishl (Rand64 i, int n) {
lua_assert(n > 0 && n < 32);
return packI((i.h << n) | (trim32(i.l) >> (32 - n)), i.l << n);
}
/* i1 ^= i2 */
static void Ixor (Rand64 *i1, Rand64 i2) {
i1->h ^= i2.h;
i1->l ^= i2.l;
}
/* return i1 + i2 */
static Rand64 Iadd (Rand64 i1, Rand64 i2) {
Rand64 result = packI(i1.h + i2.h, i1.l + i2.l);
if (trim32(result.l) < trim32(i1.l)) /* carry? */
result.h++;
return result;
}
/* return i * 5 */
static Rand64 times5 (Rand64 i) {
return Iadd(Ishl(i, 2), i); /* i * 5 == (i << 2) + i */
}
/* return i * 9 */
static Rand64 times9 (Rand64 i) {
return Iadd(Ishl(i, 3), i); /* i * 9 == (i << 3) + i */
}
/* return 'i' rotated left 'n' bits */
static Rand64 rotl (Rand64 i, int n) {
lua_assert(n > 0 && n < 32);
return packI((i.h << n) | (trim32(i.l) >> (32 - n)),
(trim32(i.h) >> (32 - n)) | (i.l << n));
}
/* for offsets larger than 32, rotate right by 64 - offset */
static Rand64 rotl1 (Rand64 i, int n) {
lua_assert(n > 32 && n < 64);
n = 64 - n;
return packI((trim32(i.h) >> n) | (i.l << (32 - n)),
(i.h << (32 - n)) | (trim32(i.l) >> n));
}
/*
** implementation of 'xoshiro256**' algorithm on 'Rand64' values
*/
static Rand64 nextrand (Rand64 *state) {
Rand64 res = times9(rotl(times5(state[1]), 7));
Rand64 t = Ishl(state[1], 17);
Ixor(&state[2], state[0]);
Ixor(&state[3], state[1]);
Ixor(&state[1], state[2]);
Ixor(&state[0], state[3]);
Ixor(&state[2], t);
state[3] = rotl1(state[3], 45);
return res;
}
/*
** Converts a 'Rand64' into a float.
*/
/* an unsigned 1 with proper type */
#define UONE ((lu_int32)1)
#if FIGS <= 32
/* 2^(-FIGS) */
#define scaleFIG (l_mathop(0.5) / (UONE << (FIGS - 1)))
/*
** get up to 32 bits from higher half, shifting right to
** throw out the extra bits.
*/
static lua_Number I2d (Rand64 x) {
lua_Number h = (lua_Number)(trim32(x.h) >> (32 - FIGS));
return h * scaleFIG;
}
#else /* 32 < FIGS <= 64 */
/* 2^(-FIGS) = 1.0 / 2^30 / 2^3 / 2^(FIGS-33) */
#define scaleFIG \
(l_mathop(1.0) / (UONE << 30) / l_mathop(8.0) / (UONE << (FIGS - 33)))
/*
** use FIGS - 32 bits from lower half, throwing out the other
** (32 - (FIGS - 32)) = (64 - FIGS) bits
*/
#define shiftLOW (64 - FIGS)
/*
** higher 32 bits go after those (FIGS - 32) bits: shiftHI = 2^(FIGS - 32)
*/
#define shiftHI ((lua_Number)(UONE << (FIGS - 33)) * l_mathop(2.0))
static lua_Number I2d (Rand64 x) {
lua_Number h = (lua_Number)trim32(x.h) * shiftHI;
lua_Number l = (lua_Number)(trim32(x.l) >> shiftLOW);
return (h + l) * scaleFIG;
}
#endif
/* convert a 'Rand64' to a 'lua_Unsigned' */
static lua_Unsigned I2UInt (Rand64 x) {
return (((lua_Unsigned)trim32(x.h) << 31) << 1) | (lua_Unsigned)trim32(x.l);
}
/* convert a 'lua_Unsigned' to a 'Rand64' */
static Rand64 Int2I (lua_Unsigned n) {
return packI((lu_int32)((n >> 31) >> 1), (lu_int32)n);
}
#endif /* } */
/*
** A state uses four 'Rand64' values.
*/
typedef struct {
Rand64 s[4];
} RanState;
/*
** Project the random integer 'ran' into the interval [0, n].
** Because 'ran' has 2^B possible values, the projection can only be
** uniform when the size of the interval is a power of 2 (exact
** division). Otherwise, to get a uniform projection into [0, n], we
** first compute 'lim', the smallest Mersenne number not smaller than
** 'n'. We then project 'ran' into the interval [0, lim]. If the result
** is inside [0, n], we are done. Otherwise, we try with another 'ran',
** until we have a result inside the interval.
*/
static lua_Unsigned project (lua_Unsigned ran, lua_Unsigned n,
RanState *state) {
if ((n & (n + 1)) == 0) /* is 'n + 1' a power of 2? */
return ran & n; /* no bias */
else {
lua_Unsigned lim = n;
/* compute the smallest (2^b - 1) not smaller than 'n' */
lim |= (lim >> 1);
lim |= (lim >> 2);
lim |= (lim >> 4);
lim |= (lim >> 8);
lim |= (lim >> 16);
#if (LUA_MAXUNSIGNED >> 31) >= 3
lim |= (lim >> 32); /* integer type has more than 32 bits */
#endif
lua_assert((lim & (lim + 1)) == 0 /* 'lim + 1' is a power of 2, */
&& lim >= n /* not smaller than 'n', */
&& (lim >> 1) < n); /* and it is the smallest one */
while ((ran &= lim) > n) /* project 'ran' into [0..lim] */
ran = I2UInt(nextrand(state->s)); /* not inside [0..n]? try again */
return ran;
}
}
static int math_random (lua_State *L) {
lua_Integer low, up;
lua_Unsigned p;
RanState *state = (RanState *)lua_touserdata(L, lua_upvalueindex(1));
Rand64 rv = nextrand(state->s); /* next pseudo-random value */
switch (lua_gettop(L)) { /* check number of arguments */
case 0: { /* no arguments */
lua_pushnumber(L, I2d(rv)); /* float between 0 and 1 */
return 1;
}
case 1: { /* only upper limit */
low = 1;
up = luaL_checkinteger(L, 1);
if (up == 0) { /* single 0 as argument? */
lua_pushinteger(L, I2UInt(rv)); /* full random integer */
return 1;
}
break;
}
case 2: { /* lower and upper limits */
low = luaL_checkinteger(L, 1);
up = luaL_checkinteger(L, 2);
break;
}
default: return luaL_error(L, "wrong number of arguments");
}
/* random integer in the interval [low, up] */
luaL_argcheck(L, low <= up, 1, "interval is empty");
/* project random integer into the interval [0, up - low] */
p = project(I2UInt(rv), (lua_Unsigned)up - (lua_Unsigned)low, state);
lua_pushinteger(L, p + (lua_Unsigned)low);
return 1;
}
static void setseed (lua_State *L, Rand64 *state,
lua_Unsigned n1, lua_Unsigned n2) {
int i;
state[0] = Int2I(n1);
state[1] = Int2I(0xff); /* avoid a zero state */
state[2] = Int2I(n2);
state[3] = Int2I(0);
for (i = 0; i < 16; i++)
nextrand(state); /* discard initial values to "spread" seed */
lua_pushinteger(L, n1);
lua_pushinteger(L, n2);
}
/*
** Set a "random" seed. To get some randomness, use the current time
** and the address of 'L' (in case the machine does address space layout
** randomization).
*/
static void randseed (lua_State *L, RanState *state) {
lua_Unsigned seed1 = (lua_Unsigned)time(NULL);
lua_Unsigned seed2 = (lua_Unsigned)(size_t)L;
setseed(L, state->s, seed1, seed2);
}
static int math_randomseed (lua_State *L) {
RanState *state = (RanState *)lua_touserdata(L, lua_upvalueindex(1));
if (lua_isnone(L, 1)) {
randseed(L, state);
}
else {
lua_Integer n1 = luaL_checkinteger(L, 1);
lua_Integer n2 = luaL_optinteger(L, 2, 0);
setseed(L, state->s, n1, n2);
}
return 2; /* return seeds */
}
static const luaL_Reg randfuncs[] = {
{"random", math_random},
{"randomseed", math_randomseed},
{NULL, NULL}
};
/*
** Register the random functions and initialize their state.
*/
static void setrandfunc (lua_State *L) {
RanState *state = (RanState *)lua_newuserdatauv(L, sizeof(RanState), 0);
randseed(L, state); /* initialize with a "random" seed */
lua_pop(L, 2); /* remove pushed seeds */
luaL_setfuncs(L, randfuncs, 1);
}
/* }================================================================== */
/*
** {==================================================================
** Deprecated functions (for compatibility only)
** ===================================================================
*/
#if defined(LUA_COMPAT_MATHLIB)
static int math_cosh (lua_State *L) {
lua_pushnumber(L, l_mathop(cosh)(luaL_checknumber(L, 1)));
return 1;
}
static int math_sinh (lua_State *L) {
lua_pushnumber(L, l_mathop(sinh)(luaL_checknumber(L, 1)));
return 1;
}
static int math_tanh (lua_State *L) {
lua_pushnumber(L, l_mathop(tanh)(luaL_checknumber(L, 1)));
return 1;
}
static int math_pow (lua_State *L) {
lua_Number x = luaL_checknumber(L, 1);
lua_Number y = luaL_checknumber(L, 2);
lua_pushnumber(L, l_mathop(pow)(x, y));
return 1;
}
static int math_frexp (lua_State *L) {
int e;
lua_pushnumber(L, l_mathop(frexp)(luaL_checknumber(L, 1), &e));
lua_pushinteger(L, e);
return 2;
}
static int math_ldexp (lua_State *L) {
lua_Number x = luaL_checknumber(L, 1);
int ep = (int)luaL_checkinteger(L, 2);
lua_pushnumber(L, l_mathop(ldexp)(x, ep));
return 1;
}
static int math_log10 (lua_State *L) {
lua_pushnumber(L, l_mathop(log10)(luaL_checknumber(L, 1)));
return 1;
}
#endif
/* }================================================================== */
static const luaL_Reg mathlib[] = {
{"abs", math_abs},
{"acos", math_acos},
{"asin", math_asin},
{"atan", math_atan},
{"ceil", math_ceil},
{"cos", math_cos},
{"deg", math_deg},
{"exp", math_exp},
{"tointeger", math_toint},
{"floor", math_floor},
{"fmod", math_fmod},
{"ult", math_ult},
{"log", math_log},
{"max", math_max},
{"min", math_min},
{"modf", math_modf},
{"rad", math_rad},
{"sin", math_sin},
{"sqrt", math_sqrt},
{"tan", math_tan},
{"type", math_type},
#if defined(LUA_COMPAT_MATHLIB)
{"atan2", math_atan},
{"cosh", math_cosh},
{"sinh", math_sinh},
{"tanh", math_tanh},
{"pow", math_pow},
{"frexp", math_frexp},
{"ldexp", math_ldexp},
{"log10", math_log10},
#endif
/* placeholders */
{"random", NULL},
{"randomseed", NULL},
{"pi", NULL},
{"huge", NULL},
{"maxinteger", NULL},
{"mininteger", NULL},
{NULL, NULL}
};
/*
** Open math library
*/
LUAMOD_API int luaopen_math (lua_State *L) {
luaL_newlib(L, mathlib);
lua_pushnumber(L, PI);
lua_setfield(L, -2, "pi");
lua_pushnumber(L, (lua_Number)HUGE_VAL);
lua_setfield(L, -2, "huge");
lua_pushinteger(L, LUA_MAXINTEGER);
lua_setfield(L, -2, "maxinteger");
lua_pushinteger(L, LUA_MININTEGER);
lua_setfield(L, -2, "mininteger");
setrandfunc(L);
return 1;
}

View File

@@ -0,0 +1,215 @@
/*
** $Id: lmem.c $
** Interface to Memory Manager
** See Copyright Notice in lua.h
*/
#define lmem_c
#define LUA_CORE
#include "lprefix.h"
#include <stddef.h>
#include "lua.h"
#include "ldebug.h"
#include "ldo.h"
#include "lgc.h"
#include "lmem.h"
#include "lobject.h"
#include "lstate.h"
/*
** About the realloc function:
** void *frealloc (void *ud, void *ptr, size_t osize, size_t nsize);
** ('osize' is the old size, 'nsize' is the new size)
**
** - frealloc(ud, p, x, 0) frees the block 'p' and returns NULL.
** Particularly, frealloc(ud, NULL, 0, 0) does nothing,
** which is equivalent to free(NULL) in ISO C.
**
** - frealloc(ud, NULL, x, s) creates a new block of size 's'
** (no matter 'x'). Returns NULL if it cannot create the new block.
**
** - otherwise, frealloc(ud, b, x, y) reallocates the block 'b' from
** size 'x' to size 'y'. Returns NULL if it cannot reallocate the
** block to the new size.
*/
/*
** Macro to call the allocation function.
*/
#define callfrealloc(g,block,os,ns) ((*g->frealloc)(g->ud, block, os, ns))
/*
** When an allocation fails, it will try again after an emergency
** collection, except when it cannot run a collection. The GC should
** not be called while the state is not fully built, as the collector
** is not yet fully initialized. Also, it should not be called when
** 'gcstopem' is true, because then the interpreter is in the middle of
** a collection step.
*/
#define cantryagain(g) (completestate(g) && !g->gcstopem)
#if defined(EMERGENCYGCTESTS)
/*
** First allocation will fail except when freeing a block (frees never
** fail) and when it cannot try again; this fail will trigger 'tryagain'
** and a full GC cycle at every allocation.
*/
static void *firsttry (global_State *g, void *block, size_t os, size_t ns) {
if (ns > 0 && cantryagain(g))
return NULL; /* fail */
else /* normal allocation */
return callfrealloc(g, block, os, ns);
}
#else
#define firsttry(g,block,os,ns) callfrealloc(g, block, os, ns)
#endif
/*
** {==================================================================
** Functions to allocate/deallocate arrays for the Parser
** ===================================================================
*/
/*
** Minimum size for arrays during parsing, to avoid overhead of
** reallocating to size 1, then 2, and then 4. All these arrays
** will be reallocated to exact sizes or erased when parsing ends.
*/
#define MINSIZEARRAY 4
void *luaM_growaux_ (lua_State *L, void *block, int nelems, int *psize,
int size_elems, int limit, const char *what) {
void *newblock;
int size = *psize;
if (nelems + 1 <= size) /* does one extra element still fit? */
return block; /* nothing to be done */
if (size >= limit / 2) { /* cannot double it? */
if (l_unlikely(size >= limit)) /* cannot grow even a little? */
luaG_runerror(L, "too many %s (limit is %d)", what, limit);
size = limit; /* still have at least one free place */
}
else {
size *= 2;
if (size < MINSIZEARRAY)
size = MINSIZEARRAY; /* minimum size */
}
lua_assert(nelems + 1 <= size && size <= limit);
/* 'limit' ensures that multiplication will not overflow */
newblock = luaM_saferealloc_(L, block, cast_sizet(*psize) * size_elems,
cast_sizet(size) * size_elems);
*psize = size; /* update only when everything else is OK */
return newblock;
}
/*
** In prototypes, the size of the array is also its number of
** elements (to save memory). So, if it cannot shrink an array
** to its number of elements, the only option is to raise an
** error.
*/
void *luaM_shrinkvector_ (lua_State *L, void *block, int *size,
int final_n, int size_elem) {
void *newblock;
size_t oldsize = cast_sizet((*size) * size_elem);
size_t newsize = cast_sizet(final_n * size_elem);
lua_assert(newsize <= oldsize);
newblock = luaM_saferealloc_(L, block, oldsize, newsize);
*size = final_n;
return newblock;
}
/* }================================================================== */
l_noret luaM_toobig (lua_State *L) {
luaG_runerror(L, "memory allocation error: block too big");
}
/*
** Free memory
*/
void luaM_free_ (lua_State *L, void *block, size_t osize) {
global_State *g = G(L);
lua_assert((osize == 0) == (block == NULL));
callfrealloc(g, block, osize, 0);
g->GCdebt -= osize;
}
/*
** In case of allocation fail, this function will do an emergency
** collection to free some memory and then try the allocation again.
*/
static void *tryagain (lua_State *L, void *block,
size_t osize, size_t nsize) {
global_State *g = G(L);
if (cantryagain(g)) {
luaC_fullgc(L, 1); /* try to free some memory... */
return callfrealloc(g, block, osize, nsize); /* try again */
}
else return NULL; /* cannot run an emergency collection */
}
/*
** Generic allocation routine.
*/
void *luaM_realloc_ (lua_State *L, void *block, size_t osize, size_t nsize) {
void *newblock;
global_State *g = G(L);
lua_assert((osize == 0) == (block == NULL));
newblock = firsttry(g, block, osize, nsize);
if (l_unlikely(newblock == NULL && nsize > 0)) {
newblock = tryagain(L, block, osize, nsize);
if (newblock == NULL) /* still no memory? */
return NULL; /* do not update 'GCdebt' */
}
lua_assert((nsize == 0) == (newblock == NULL));
g->GCdebt = (g->GCdebt + nsize) - osize;
return newblock;
}
void *luaM_saferealloc_ (lua_State *L, void *block, size_t osize,
size_t nsize) {
void *newblock = luaM_realloc_(L, block, osize, nsize);
if (l_unlikely(newblock == NULL && nsize > 0)) /* allocation failed? */
luaM_error(L);
return newblock;
}
void *luaM_malloc_ (lua_State *L, size_t size, int tag) {
if (size == 0)
return NULL; /* that's all */
else {
global_State *g = G(L);
void *newblock = firsttry(g, NULL, tag, size);
if (l_unlikely(newblock == NULL)) {
newblock = tryagain(L, NULL, tag, size);
if (newblock == NULL)
luaM_error(L);
}
g->GCdebt += size;
return newblock;
}
}

View File

@@ -0,0 +1,93 @@
/*
** $Id: lmem.h $
** Interface to Memory Manager
** See Copyright Notice in lua.h
*/
#ifndef lmem_h
#define lmem_h
#include <stddef.h>
#include "llimits.h"
#include "lua.h"
#define luaM_error(L) luaD_throw(L, LUA_ERRMEM)
/*
** This macro tests whether it is safe to multiply 'n' by the size of
** type 't' without overflows. Because 'e' is always constant, it avoids
** the runtime division MAX_SIZET/(e).
** (The macro is somewhat complex to avoid warnings: The 'sizeof'
** comparison avoids a runtime comparison when overflow cannot occur.
** The compiler should be able to optimize the real test by itself, but
** when it does it, it may give a warning about "comparison is always
** false due to limited range of data type"; the +1 tricks the compiler,
** avoiding this warning but also this optimization.)
*/
#define luaM_testsize(n,e) \
(sizeof(n) >= sizeof(size_t) && cast_sizet((n)) + 1 > MAX_SIZET/(e))
#define luaM_checksize(L,n,e) \
(luaM_testsize(n,e) ? luaM_toobig(L) : cast_void(0))
/*
** Computes the minimum between 'n' and 'MAX_SIZET/sizeof(t)', so that
** the result is not larger than 'n' and cannot overflow a 'size_t'
** when multiplied by the size of type 't'. (Assumes that 'n' is an
** 'int' or 'unsigned int' and that 'int' is not larger than 'size_t'.)
*/
#define luaM_limitN(n,t) \
((cast_sizet(n) <= MAX_SIZET/sizeof(t)) ? (n) : \
cast_uint((MAX_SIZET/sizeof(t))))
/*
** Arrays of chars do not need any test
*/
#define luaM_reallocvchar(L,b,on,n) \
cast_charp(luaM_saferealloc_(L, (b), (on)*sizeof(char), (n)*sizeof(char)))
#define luaM_freemem(L, b, s) luaM_free_(L, (b), (s))
#define luaM_free(L, b) luaM_free_(L, (b), sizeof(*(b)))
#define luaM_freearray(L, b, n) luaM_free_(L, (b), (n)*sizeof(*(b)))
#define luaM_new(L,t) cast(t*, luaM_malloc_(L, sizeof(t), 0))
#define luaM_newvector(L,n,t) cast(t*, luaM_malloc_(L, (n)*sizeof(t), 0))
#define luaM_newvectorchecked(L,n,t) \
(luaM_checksize(L,n,sizeof(t)), luaM_newvector(L,n,t))
#define luaM_newobject(L,tag,s) luaM_malloc_(L, (s), tag)
#define luaM_growvector(L,v,nelems,size,t,limit,e) \
((v)=cast(t *, luaM_growaux_(L,v,nelems,&(size),sizeof(t), \
luaM_limitN(limit,t),e)))
#define luaM_reallocvector(L, v,oldn,n,t) \
(cast(t *, luaM_realloc_(L, v, cast_sizet(oldn) * sizeof(t), \
cast_sizet(n) * sizeof(t))))
#define luaM_shrinkvector(L,v,size,fs,t) \
((v)=cast(t *, luaM_shrinkvector_(L, v, &(size), fs, sizeof(t))))
LUAI_FUNC l_noret luaM_toobig (lua_State *L);
/* not to be called directly */
LUAI_FUNC void *luaM_realloc_ (lua_State *L, void *block, size_t oldsize,
size_t size);
LUAI_FUNC void *luaM_saferealloc_ (lua_State *L, void *block, size_t oldsize,
size_t size);
LUAI_FUNC void luaM_free_ (lua_State *L, void *block, size_t osize);
LUAI_FUNC void *luaM_growaux_ (lua_State *L, void *block, int nelems,
int *size, int size_elem, int limit,
const char *what);
LUAI_FUNC void *luaM_shrinkvector_ (lua_State *L, void *block, int *nelem,
int final_n, int size_elem);
LUAI_FUNC void *luaM_malloc_ (lua_State *L, size_t size, int tag);
#endif

View File

@@ -0,0 +1,758 @@
/*
** $Id: loadlib.c $
** Dynamic library loader for Lua
** See Copyright Notice in lua.h
**
** This module contains an implementation of loadlib for Unix systems
** that have dlfcn, an implementation for Windows, and a stub for other
** systems.
*/
#define loadlib_c
#define LUA_LIB
#include "lprefix.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
/*
** LUA_CSUBSEP is the character that replaces dots in submodule names
** when searching for a C loader.
** LUA_LSUBSEP is the character that replaces dots in submodule names
** when searching for a Lua loader.
*/
#if !defined(LUA_CSUBSEP)
#define LUA_CSUBSEP LUA_DIRSEP
#endif
#if !defined(LUA_LSUBSEP)
#define LUA_LSUBSEP LUA_DIRSEP
#endif
/* prefix for open functions in C libraries */
#define LUA_POF "luaopen_"
/* separator for open functions in C libraries */
#define LUA_OFSEP "_"
/*
** key for table in the registry that keeps handles
** for all loaded C libraries
*/
static const char *const CLIBS = "_CLIBS";
#define LIB_FAIL "open"
#define setprogdir(L) ((void)0)
/*
** Special type equivalent to '(void*)' for functions in gcc
** (to suppress warnings when converting function pointers)
*/
typedef void (*voidf)(void);
/*
** system-dependent functions
*/
/*
** unload library 'lib'
*/
static void lsys_unloadlib (void *lib);
/*
** load C library in file 'path'. If 'seeglb', load with all names in
** the library global.
** Returns the library; in case of error, returns NULL plus an
** error string in the stack.
*/
static void *lsys_load (lua_State *L, const char *path, int seeglb);
/*
** Try to find a function named 'sym' in library 'lib'.
** Returns the function; in case of error, returns NULL plus an
** error string in the stack.
*/
static lua_CFunction lsys_sym (lua_State *L, void *lib, const char *sym);
#if defined(LUA_USE_DLOPEN) /* { */
/*
** {========================================================================
** This is an implementation of loadlib based on the dlfcn interface.
** The dlfcn interface is available in Linux, SunOS, Solaris, IRIX, FreeBSD,
** NetBSD, AIX 4.2, HPUX 11, and probably most other Unix flavors, at least
** as an emulation layer on top of native functions.
** =========================================================================
*/
#include <dlfcn.h>
/*
** Macro to convert pointer-to-void* to pointer-to-function. This cast
** is undefined according to ISO C, but POSIX assumes that it works.
** (The '__extension__' in gnu compilers is only to avoid warnings.)
*/
#if defined(__GNUC__)
#define cast_func(p) (__extension__ (lua_CFunction)(p))
#else
#define cast_func(p) ((lua_CFunction)(p))
#endif
static void lsys_unloadlib (void *lib) {
dlclose(lib);
}
static void *lsys_load (lua_State *L, const char *path, int seeglb) {
void *lib = dlopen(path, RTLD_NOW | (seeglb ? RTLD_GLOBAL : RTLD_LOCAL));
if (l_unlikely(lib == NULL))
lua_pushstring(L, dlerror());
return lib;
}
static lua_CFunction lsys_sym (lua_State *L, void *lib, const char *sym) {
lua_CFunction f = cast_func(dlsym(lib, sym));
if (l_unlikely(f == NULL))
lua_pushstring(L, dlerror());
return f;
}
/* }====================================================== */
#elif defined(LUA_DL_DLL) /* }{ */
/*
** {======================================================================
** This is an implementation of loadlib for Windows using native functions.
** =======================================================================
*/
#include <windows.h>
/*
** optional flags for LoadLibraryEx
*/
#if !defined(LUA_LLE_FLAGS)
#define LUA_LLE_FLAGS 0
#endif
#undef setprogdir
/*
** Replace in the path (on the top of the stack) any occurrence
** of LUA_EXEC_DIR with the executable's path.
*/
static void setprogdir (lua_State *L) {
char buff[MAX_PATH + 1];
char *lb;
DWORD nsize = sizeof(buff)/sizeof(char);
DWORD n = GetModuleFileNameA(NULL, buff, nsize); /* get exec. name */
if (n == 0 || n == nsize || (lb = strrchr(buff, '\\')) == NULL)
luaL_error(L, "unable to get ModuleFileName");
else {
*lb = '\0'; /* cut name on the last '\\' to get the path */
luaL_gsub(L, lua_tostring(L, -1), LUA_EXEC_DIR, buff);
lua_remove(L, -2); /* remove original string */
}
}
static void pusherror (lua_State *L) {
int error = GetLastError();
char buffer[128];
if (FormatMessageA(FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, error, 0, buffer, sizeof(buffer)/sizeof(char), NULL))
lua_pushstring(L, buffer);
else
lua_pushfstring(L, "system error %d\n", error);
}
static void lsys_unloadlib (void *lib) {
FreeLibrary((HMODULE)lib);
}
static void *lsys_load (lua_State *L, const char *path, int seeglb) {
HMODULE lib = LoadLibraryExA(path, NULL, LUA_LLE_FLAGS);
(void)(seeglb); /* not used: symbols are 'global' by default */
if (lib == NULL) pusherror(L);
return lib;
}
static lua_CFunction lsys_sym (lua_State *L, void *lib, const char *sym) {
lua_CFunction f = (lua_CFunction)(voidf)GetProcAddress((HMODULE)lib, sym);
if (f == NULL) pusherror(L);
return f;
}
/* }====================================================== */
#else /* }{ */
/*
** {======================================================
** Fallback for other systems
** =======================================================
*/
#undef LIB_FAIL
#define LIB_FAIL "absent"
#define DLMSG "dynamic libraries not enabled; check your Lua installation"
static void lsys_unloadlib (void *lib) {
(void)(lib); /* not used */
}
static void *lsys_load (lua_State *L, const char *path, int seeglb) {
(void)(path); (void)(seeglb); /* not used */
lua_pushliteral(L, DLMSG);
return NULL;
}
static lua_CFunction lsys_sym (lua_State *L, void *lib, const char *sym) {
(void)(lib); (void)(sym); /* not used */
lua_pushliteral(L, DLMSG);
return NULL;
}
/* }====================================================== */
#endif /* } */
/*
** {==================================================================
** Set Paths
** ===================================================================
*/
/*
** LUA_PATH_VAR and LUA_CPATH_VAR are the names of the environment
** variables that Lua check to set its paths.
*/
#if !defined(LUA_PATH_VAR)
#define LUA_PATH_VAR "LUA_PATH"
#endif
#if !defined(LUA_CPATH_VAR)
#define LUA_CPATH_VAR "LUA_CPATH"
#endif
/*
** return registry.LUA_NOENV as a boolean
*/
static int noenv (lua_State *L) {
int b;
lua_getfield(L, LUA_REGISTRYINDEX, "LUA_NOENV");
b = lua_toboolean(L, -1);
lua_pop(L, 1); /* remove value */
return b;
}
/*
** Set a path
*/
static void setpath (lua_State *L, const char *fieldname,
const char *envname,
const char *dft) {
const char *dftmark;
const char *nver = lua_pushfstring(L, "%s%s", envname, LUA_VERSUFFIX);
const char *path = getenv(nver); /* try versioned name */
if (path == NULL) /* no versioned environment variable? */
path = getenv(envname); /* try unversioned name */
if (path == NULL || noenv(L)) /* no environment variable? */
lua_pushstring(L, dft); /* use default */
else if ((dftmark = strstr(path, LUA_PATH_SEP LUA_PATH_SEP)) == NULL)
lua_pushstring(L, path); /* nothing to change */
else { /* path contains a ";;": insert default path in its place */
size_t len = strlen(path);
luaL_Buffer b;
luaL_buffinit(L, &b);
if (path < dftmark) { /* is there a prefix before ';;'? */
luaL_addlstring(&b, path, dftmark - path); /* add it */
luaL_addchar(&b, *LUA_PATH_SEP);
}
luaL_addstring(&b, dft); /* add default */
if (dftmark < path + len - 2) { /* is there a suffix after ';;'? */
luaL_addchar(&b, *LUA_PATH_SEP);
luaL_addlstring(&b, dftmark + 2, (path + len - 2) - dftmark);
}
luaL_pushresult(&b);
}
setprogdir(L);
lua_setfield(L, -3, fieldname); /* package[fieldname] = path value */
lua_pop(L, 1); /* pop versioned variable name ('nver') */
}
/* }================================================================== */
/*
** return registry.CLIBS[path]
*/
static void *checkclib (lua_State *L, const char *path) {
void *plib;
lua_getfield(L, LUA_REGISTRYINDEX, CLIBS);
lua_getfield(L, -1, path);
plib = lua_touserdata(L, -1); /* plib = CLIBS[path] */
lua_pop(L, 2); /* pop CLIBS table and 'plib' */
return plib;
}
/*
** registry.CLIBS[path] = plib -- for queries
** registry.CLIBS[#CLIBS + 1] = plib -- also keep a list of all libraries
*/
static void addtoclib (lua_State *L, const char *path, void *plib) {
lua_getfield(L, LUA_REGISTRYINDEX, CLIBS);
lua_pushlightuserdata(L, plib);
lua_pushvalue(L, -1);
lua_setfield(L, -3, path); /* CLIBS[path] = plib */
lua_rawseti(L, -2, luaL_len(L, -2) + 1); /* CLIBS[#CLIBS + 1] = plib */
lua_pop(L, 1); /* pop CLIBS table */
}
/*
** __gc tag method for CLIBS table: calls 'lsys_unloadlib' for all lib
** handles in list CLIBS
*/
static int gctm (lua_State *L) {
lua_Integer n = luaL_len(L, 1);
for (; n >= 1; n--) { /* for each handle, in reverse order */
lua_rawgeti(L, 1, n); /* get handle CLIBS[n] */
lsys_unloadlib(lua_touserdata(L, -1));
lua_pop(L, 1); /* pop handle */
}
return 0;
}
/* error codes for 'lookforfunc' */
#define ERRLIB 1
#define ERRFUNC 2
/*
** Look for a C function named 'sym' in a dynamically loaded library
** 'path'.
** First, check whether the library is already loaded; if not, try
** to load it.
** Then, if 'sym' is '*', return true (as library has been loaded).
** Otherwise, look for symbol 'sym' in the library and push a
** C function with that symbol.
** Return 0 and 'true' or a function in the stack; in case of
** errors, return an error code and an error message in the stack.
*/
static int lookforfunc (lua_State *L, const char *path, const char *sym) {
void *reg = checkclib(L, path); /* check loaded C libraries */
if (reg == NULL) { /* must load library? */
reg = lsys_load(L, path, *sym == '*'); /* global symbols if 'sym'=='*' */
if (reg == NULL) return ERRLIB; /* unable to load library */
addtoclib(L, path, reg);
}
if (*sym == '*') { /* loading only library (no function)? */
lua_pushboolean(L, 1); /* return 'true' */
return 0; /* no errors */
}
else {
lua_CFunction f = lsys_sym(L, reg, sym);
if (f == NULL)
return ERRFUNC; /* unable to find function */
lua_pushcfunction(L, f); /* else create new function */
return 0; /* no errors */
}
}
static int ll_loadlib (lua_State *L) {
const char *path = luaL_checkstring(L, 1);
const char *init = luaL_checkstring(L, 2);
int stat = lookforfunc(L, path, init);
if (l_likely(stat == 0)) /* no errors? */
return 1; /* return the loaded function */
else { /* error; error message is on stack top */
luaL_pushfail(L);
lua_insert(L, -2);
lua_pushstring(L, (stat == ERRLIB) ? LIB_FAIL : "init");
return 3; /* return fail, error message, and where */
}
}
/*
** {======================================================
** 'require' function
** =======================================================
*/
static int readable (const char *filename) {
FILE *f = fopen(filename, "r"); /* try to open file */
if (f == NULL) return 0; /* open failed */
fclose(f);
return 1;
}
/*
** Get the next name in '*path' = 'name1;name2;name3;...', changing
** the ending ';' to '\0' to create a zero-terminated string. Return
** NULL when list ends.
*/
static const char *getnextfilename (char **path, char *end) {
char *sep;
char *name = *path;
if (name == end)
return NULL; /* no more names */
else if (*name == '\0') { /* from previous iteration? */
*name = *LUA_PATH_SEP; /* restore separator */
name++; /* skip it */
}
sep = strchr(name, *LUA_PATH_SEP); /* find next separator */
if (sep == NULL) /* separator not found? */
sep = end; /* name goes until the end */
*sep = '\0'; /* finish file name */
*path = sep; /* will start next search from here */
return name;
}
/*
** Given a path such as ";blabla.so;blublu.so", pushes the string
**
** no file 'blabla.so'
** no file 'blublu.so'
*/
static void pusherrornotfound (lua_State *L, const char *path) {
luaL_Buffer b;
luaL_buffinit(L, &b);
luaL_addstring(&b, "no file '");
luaL_addgsub(&b, path, LUA_PATH_SEP, "'\n\tno file '");
luaL_addstring(&b, "'");
luaL_pushresult(&b);
}
static const char *searchpath (lua_State *L, const char *name,
const char *path,
const char *sep,
const char *dirsep) {
luaL_Buffer buff;
char *pathname; /* path with name inserted */
char *endpathname; /* its end */
const char *filename;
/* separator is non-empty and appears in 'name'? */
if (*sep != '\0' && strchr(name, *sep) != NULL)
name = luaL_gsub(L, name, sep, dirsep); /* replace it by 'dirsep' */
luaL_buffinit(L, &buff);
/* add path to the buffer, replacing marks ('?') with the file name */
luaL_addgsub(&buff, path, LUA_PATH_MARK, name);
luaL_addchar(&buff, '\0');
pathname = luaL_buffaddr(&buff); /* writable list of file names */
endpathname = pathname + luaL_bufflen(&buff) - 1;
while ((filename = getnextfilename(&pathname, endpathname)) != NULL) {
if (readable(filename)) /* does file exist and is readable? */
return lua_pushstring(L, filename); /* save and return name */
}
luaL_pushresult(&buff); /* push path to create error message */
pusherrornotfound(L, lua_tostring(L, -1)); /* create error message */
return NULL; /* not found */
}
static int ll_searchpath (lua_State *L) {
const char *f = searchpath(L, luaL_checkstring(L, 1),
luaL_checkstring(L, 2),
luaL_optstring(L, 3, "."),
luaL_optstring(L, 4, LUA_DIRSEP));
if (f != NULL) return 1;
else { /* error message is on top of the stack */
luaL_pushfail(L);
lua_insert(L, -2);
return 2; /* return fail + error message */
}
}
static const char *findfile (lua_State *L, const char *name,
const char *pname,
const char *dirsep) {
const char *path;
lua_getfield(L, lua_upvalueindex(1), pname);
path = lua_tostring(L, -1);
if (l_unlikely(path == NULL))
luaL_error(L, "'package.%s' must be a string", pname);
return searchpath(L, name, path, ".", dirsep);
}
static int checkload (lua_State *L, int stat, const char *filename) {
if (l_likely(stat)) { /* module loaded successfully? */
lua_pushstring(L, filename); /* will be 2nd argument to module */
return 2; /* return open function and file name */
}
else
return luaL_error(L, "error loading module '%s' from file '%s':\n\t%s",
lua_tostring(L, 1), filename, lua_tostring(L, -1));
}
static int searcher_Lua (lua_State *L) {
const char *filename;
const char *name = luaL_checkstring(L, 1);
filename = findfile(L, name, "path", LUA_LSUBSEP);
if (filename == NULL) return 1; /* module not found in this path */
return checkload(L, (luaL_loadfile(L, filename) == LUA_OK), filename);
}
/*
** Try to find a load function for module 'modname' at file 'filename'.
** First, change '.' to '_' in 'modname'; then, if 'modname' has
** the form X-Y (that is, it has an "ignore mark"), build a function
** name "luaopen_X" and look for it. (For compatibility, if that
** fails, it also tries "luaopen_Y".) If there is no ignore mark,
** look for a function named "luaopen_modname".
*/
static int loadfunc (lua_State *L, const char *filename, const char *modname) {
const char *openfunc;
const char *mark;
modname = luaL_gsub(L, modname, ".", LUA_OFSEP);
mark = strchr(modname, *LUA_IGMARK);
if (mark) {
int stat;
openfunc = lua_pushlstring(L, modname, mark - modname);
openfunc = lua_pushfstring(L, LUA_POF"%s", openfunc);
stat = lookforfunc(L, filename, openfunc);
if (stat != ERRFUNC) return stat;
modname = mark + 1; /* else go ahead and try old-style name */
}
openfunc = lua_pushfstring(L, LUA_POF"%s", modname);
return lookforfunc(L, filename, openfunc);
}
static int searcher_C (lua_State *L) {
const char *name = luaL_checkstring(L, 1);
const char *filename = findfile(L, name, "cpath", LUA_CSUBSEP);
if (filename == NULL) return 1; /* module not found in this path */
return checkload(L, (loadfunc(L, filename, name) == 0), filename);
}
static int searcher_Croot (lua_State *L) {
const char *filename;
const char *name = luaL_checkstring(L, 1);
const char *p = strchr(name, '.');
int stat;
if (p == NULL) return 0; /* is root */
lua_pushlstring(L, name, p - name);
filename = findfile(L, lua_tostring(L, -1), "cpath", LUA_CSUBSEP);
if (filename == NULL) return 1; /* root not found */
if ((stat = loadfunc(L, filename, name)) != 0) {
if (stat != ERRFUNC)
return checkload(L, 0, filename); /* real error */
else { /* open function not found */
lua_pushfstring(L, "no module '%s' in file '%s'", name, filename);
return 1;
}
}
lua_pushstring(L, filename); /* will be 2nd argument to module */
return 2;
}
static int searcher_preload (lua_State *L) {
const char *name = luaL_checkstring(L, 1);
lua_getfield(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE);
if (lua_getfield(L, -1, name) == LUA_TNIL) { /* not found? */
lua_pushfstring(L, "no field package.preload['%s']", name);
return 1;
}
else {
lua_pushliteral(L, ":preload:");
return 2;
}
}
static void findloader (lua_State *L, const char *name) {
int i;
luaL_Buffer msg; /* to build error message */
/* push 'package.searchers' to index 3 in the stack */
if (l_unlikely(lua_getfield(L, lua_upvalueindex(1), "searchers")
!= LUA_TTABLE))
luaL_error(L, "'package.searchers' must be a table");
luaL_buffinit(L, &msg);
/* iterate over available searchers to find a loader */
for (i = 1; ; i++) {
luaL_addstring(&msg, "\n\t"); /* error-message prefix */
if (l_unlikely(lua_rawgeti(L, 3, i) == LUA_TNIL)) { /* no more searchers? */
lua_pop(L, 1); /* remove nil */
luaL_buffsub(&msg, 2); /* remove prefix */
luaL_pushresult(&msg); /* create error message */
luaL_error(L, "module '%s' not found:%s", name, lua_tostring(L, -1));
}
lua_pushstring(L, name);
lua_call(L, 1, 2); /* call it */
if (lua_isfunction(L, -2)) /* did it find a loader? */
return; /* module loader found */
else if (lua_isstring(L, -2)) { /* searcher returned error message? */
lua_pop(L, 1); /* remove extra return */
luaL_addvalue(&msg); /* concatenate error message */
}
else { /* no error message */
lua_pop(L, 2); /* remove both returns */
luaL_buffsub(&msg, 2); /* remove prefix */
}
}
}
static int ll_require (lua_State *L) {
const char *name = luaL_checkstring(L, 1);
lua_settop(L, 1); /* LOADED table will be at index 2 */
lua_getfield(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
lua_getfield(L, 2, name); /* LOADED[name] */
if (lua_toboolean(L, -1)) /* is it there? */
return 1; /* package is already loaded */
/* else must load package */
lua_pop(L, 1); /* remove 'getfield' result */
findloader(L, name);
lua_rotate(L, -2, 1); /* function <-> loader data */
lua_pushvalue(L, 1); /* name is 1st argument to module loader */
lua_pushvalue(L, -3); /* loader data is 2nd argument */
/* stack: ...; loader data; loader function; mod. name; loader data */
lua_call(L, 2, 1); /* run loader to load module */
/* stack: ...; loader data; result from loader */
if (!lua_isnil(L, -1)) /* non-nil return? */
lua_setfield(L, 2, name); /* LOADED[name] = returned value */
else
lua_pop(L, 1); /* pop nil */
if (lua_getfield(L, 2, name) == LUA_TNIL) { /* module set no value? */
lua_pushboolean(L, 1); /* use true as result */
lua_copy(L, -1, -2); /* replace loader result */
lua_setfield(L, 2, name); /* LOADED[name] = true */
}
lua_rotate(L, -2, 1); /* loader data <-> module result */
return 2; /* return module result and loader data */
}
/* }====================================================== */
static const luaL_Reg pk_funcs[] = {
{"loadlib", ll_loadlib},
{"searchpath", ll_searchpath},
/* placeholders */
{"preload", NULL},
{"cpath", NULL},
{"path", NULL},
{"searchers", NULL},
{"loaded", NULL},
{NULL, NULL}
};
static const luaL_Reg ll_funcs[] = {
{"require", ll_require},
{NULL, NULL}
};
static void createsearcherstable (lua_State *L) {
static const lua_CFunction searchers[] = {
searcher_preload,
searcher_Lua,
searcher_C,
searcher_Croot,
NULL
};
int i;
/* create 'searchers' table */
lua_createtable(L, sizeof(searchers)/sizeof(searchers[0]) - 1, 0);
/* fill it with predefined searchers */
for (i=0; searchers[i] != NULL; i++) {
lua_pushvalue(L, -2); /* set 'package' as upvalue for all searchers */
lua_pushcclosure(L, searchers[i], 1);
lua_rawseti(L, -2, i+1);
}
lua_setfield(L, -2, "searchers"); /* put it in field 'searchers' */
}
/*
** create table CLIBS to keep track of loaded C libraries,
** setting a finalizer to close all libraries when closing state.
*/
static void createclibstable (lua_State *L) {
luaL_getsubtable(L, LUA_REGISTRYINDEX, CLIBS); /* create CLIBS table */
lua_createtable(L, 0, 1); /* create metatable for CLIBS */
lua_pushcfunction(L, gctm);
lua_setfield(L, -2, "__gc"); /* set finalizer for CLIBS table */
lua_setmetatable(L, -2);
}
LUAMOD_API int luaopen_package (lua_State *L) {
createclibstable(L);
luaL_newlib(L, pk_funcs); /* create 'package' table */
createsearcherstable(L);
/* set paths */
setpath(L, "path", LUA_PATH_VAR, LUA_PATH_DEFAULT);
setpath(L, "cpath", LUA_CPATH_VAR, LUA_CPATH_DEFAULT);
/* store config information */
lua_pushliteral(L, LUA_DIRSEP "\n" LUA_PATH_SEP "\n" LUA_PATH_MARK "\n"
LUA_EXEC_DIR "\n" LUA_IGMARK "\n");
lua_setfield(L, -2, "config");
/* set field 'loaded' */
luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
lua_setfield(L, -2, "loaded");
/* set field 'preload' */
luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE);
lua_setfield(L, -2, "preload");
lua_pushglobaltable(L);
lua_pushvalue(L, -2); /* set 'package' as upvalue for next lib */
luaL_setfuncs(L, ll_funcs, 1); /* open lib into global table */
lua_pop(L, 1); /* pop global table */
return 1; /* return 'package' table */
}

View File

@@ -0,0 +1,602 @@
/*
** $Id: lobject.c $
** Some generic functions over Lua objects
** See Copyright Notice in lua.h
*/
#define lobject_c
#define LUA_CORE
#include "lprefix.h"
#include <locale.h>
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lua.h"
#include "lctype.h"
#include "ldebug.h"
#include "ldo.h"
#include "lmem.h"
#include "lobject.h"
#include "lstate.h"
#include "lstring.h"
#include "lvm.h"
/*
** Computes ceil(log2(x))
*/
int luaO_ceillog2 (unsigned int x) {
static const lu_byte log_2[256] = { /* log_2[i] = ceil(log2(i - 1)) */
0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8
};
int l = 0;
x--;
while (x >= 256) { l += 8; x >>= 8; }
return l + log_2[x];
}
static lua_Integer intarith (lua_State *L, int op, lua_Integer v1,
lua_Integer v2) {
switch (op) {
case LUA_OPADD: return intop(+, v1, v2);
case LUA_OPSUB:return intop(-, v1, v2);
case LUA_OPMUL:return intop(*, v1, v2);
case LUA_OPMOD: return luaV_mod(L, v1, v2);
case LUA_OPIDIV: return luaV_idiv(L, v1, v2);
case LUA_OPBAND: return intop(&, v1, v2);
case LUA_OPBOR: return intop(|, v1, v2);
case LUA_OPBXOR: return intop(^, v1, v2);
case LUA_OPSHL: return luaV_shiftl(v1, v2);
case LUA_OPSHR: return luaV_shiftr(v1, v2);
case LUA_OPUNM: return intop(-, 0, v1);
case LUA_OPBNOT: return intop(^, ~l_castS2U(0), v1);
default: lua_assert(0); return 0;
}
}
static lua_Number numarith (lua_State *L, int op, lua_Number v1,
lua_Number v2) {
switch (op) {
case LUA_OPADD: return luai_numadd(L, v1, v2);
case LUA_OPSUB: return luai_numsub(L, v1, v2);
case LUA_OPMUL: return luai_nummul(L, v1, v2);
case LUA_OPDIV: return luai_numdiv(L, v1, v2);
case LUA_OPPOW: return luai_numpow(L, v1, v2);
case LUA_OPIDIV: return luai_numidiv(L, v1, v2);
case LUA_OPUNM: return luai_numunm(L, v1);
case LUA_OPMOD: return luaV_modf(L, v1, v2);
default: lua_assert(0); return 0;
}
}
int luaO_rawarith (lua_State *L, int op, const TValue *p1, const TValue *p2,
TValue *res) {
switch (op) {
case LUA_OPBAND: case LUA_OPBOR: case LUA_OPBXOR:
case LUA_OPSHL: case LUA_OPSHR:
case LUA_OPBNOT: { /* operate only on integers */
lua_Integer i1; lua_Integer i2;
if (tointegerns(p1, &i1) && tointegerns(p2, &i2)) {
setivalue(res, intarith(L, op, i1, i2));
return 1;
}
else return 0; /* fail */
}
case LUA_OPDIV: case LUA_OPPOW: { /* operate only on floats */
lua_Number n1; lua_Number n2;
if (tonumberns(p1, n1) && tonumberns(p2, n2)) {
setfltvalue(res, numarith(L, op, n1, n2));
return 1;
}
else return 0; /* fail */
}
default: { /* other operations */
lua_Number n1; lua_Number n2;
if (ttisinteger(p1) && ttisinteger(p2)) {
setivalue(res, intarith(L, op, ivalue(p1), ivalue(p2)));
return 1;
}
else if (tonumberns(p1, n1) && tonumberns(p2, n2)) {
setfltvalue(res, numarith(L, op, n1, n2));
return 1;
}
else return 0; /* fail */
}
}
}
void luaO_arith (lua_State *L, int op, const TValue *p1, const TValue *p2,
StkId res) {
if (!luaO_rawarith(L, op, p1, p2, s2v(res))) {
/* could not perform raw operation; try metamethod */
luaT_trybinTM(L, p1, p2, res, cast(TMS, (op - LUA_OPADD) + TM_ADD));
}
}
int luaO_hexavalue (int c) {
if (lisdigit(c)) return c - '0';
else return (ltolower(c) - 'a') + 10;
}
static int isneg (const char **s) {
if (**s == '-') { (*s)++; return 1; }
else if (**s == '+') (*s)++;
return 0;
}
/*
** {==================================================================
** Lua's implementation for 'lua_strx2number'
** ===================================================================
*/
#if !defined(lua_strx2number)
/* maximum number of significant digits to read (to avoid overflows
even with single floats) */
#define MAXSIGDIG 30
/*
** convert a hexadecimal numeric string to a number, following
** C99 specification for 'strtod'
*/
static lua_Number lua_strx2number (const char *s, char **endptr) {
int dot = lua_getlocaledecpoint();
lua_Number r = l_mathop(0.0); /* result (accumulator) */
int sigdig = 0; /* number of significant digits */
int nosigdig = 0; /* number of non-significant digits */
int e = 0; /* exponent correction */
int neg; /* 1 if number is negative */
int hasdot = 0; /* true after seen a dot */
*endptr = cast_charp(s); /* nothing is valid yet */
while (lisspace(cast_uchar(*s))) s++; /* skip initial spaces */
neg = isneg(&s); /* check sign */
if (!(*s == '0' && (*(s + 1) == 'x' || *(s + 1) == 'X'))) /* check '0x' */
return l_mathop(0.0); /* invalid format (no '0x') */
for (s += 2; ; s++) { /* skip '0x' and read numeral */
if (*s == dot) {
if (hasdot) break; /* second dot? stop loop */
else hasdot = 1;
}
else if (lisxdigit(cast_uchar(*s))) {
if (sigdig == 0 && *s == '0') /* non-significant digit (zero)? */
nosigdig++;
else if (++sigdig <= MAXSIGDIG) /* can read it without overflow? */
r = (r * l_mathop(16.0)) + luaO_hexavalue(*s);
else e++; /* too many digits; ignore, but still count for exponent */
if (hasdot) e--; /* decimal digit? correct exponent */
}
else break; /* neither a dot nor a digit */
}
if (nosigdig + sigdig == 0) /* no digits? */
return l_mathop(0.0); /* invalid format */
*endptr = cast_charp(s); /* valid up to here */
e *= 4; /* each digit multiplies/divides value by 2^4 */
if (*s == 'p' || *s == 'P') { /* exponent part? */
int exp1 = 0; /* exponent value */
int neg1; /* exponent sign */
s++; /* skip 'p' */
neg1 = isneg(&s); /* sign */
if (!lisdigit(cast_uchar(*s)))
return l_mathop(0.0); /* invalid; must have at least one digit */
while (lisdigit(cast_uchar(*s))) /* read exponent */
exp1 = exp1 * 10 + *(s++) - '0';
if (neg1) exp1 = -exp1;
e += exp1;
*endptr = cast_charp(s); /* valid up to here */
}
if (neg) r = -r;
return l_mathop(ldexp)(r, e);
}
#endif
/* }====================================================== */
/* maximum length of a numeral to be converted to a number */
#if !defined (L_MAXLENNUM)
#define L_MAXLENNUM 200
#endif
/*
** Convert string 's' to a Lua number (put in 'result'). Return NULL on
** fail or the address of the ending '\0' on success. ('mode' == 'x')
** means a hexadecimal numeral.
*/
static const char *l_str2dloc (const char *s, lua_Number *result, int mode) {
char *endptr;
*result = (mode == 'x') ? lua_strx2number(s, &endptr) /* try to convert */
: lua_str2number(s, &endptr);
if (endptr == s) return NULL; /* nothing recognized? */
while (lisspace(cast_uchar(*endptr))) endptr++; /* skip trailing spaces */
return (*endptr == '\0') ? endptr : NULL; /* OK iff no trailing chars */
}
/*
** Convert string 's' to a Lua number (put in 'result') handling the
** current locale.
** This function accepts both the current locale or a dot as the radix
** mark. If the conversion fails, it may mean number has a dot but
** locale accepts something else. In that case, the code copies 's'
** to a buffer (because 's' is read-only), changes the dot to the
** current locale radix mark, and tries to convert again.
** The variable 'mode' checks for special characters in the string:
** - 'n' means 'inf' or 'nan' (which should be rejected)
** - 'x' means a hexadecimal numeral
** - '.' just optimizes the search for the common case (no special chars)
*/
static const char *l_str2d (const char *s, lua_Number *result) {
const char *endptr;
const char *pmode = strpbrk(s, ".xXnN"); /* look for special chars */
int mode = pmode ? ltolower(cast_uchar(*pmode)) : 0;
if (mode == 'n') /* reject 'inf' and 'nan' */
return NULL;
endptr = l_str2dloc(s, result, mode); /* try to convert */
if (endptr == NULL) { /* failed? may be a different locale */
char buff[L_MAXLENNUM + 1];
const char *pdot = strchr(s, '.');
if (pdot == NULL || strlen(s) > L_MAXLENNUM)
return NULL; /* string too long or no dot; fail */
strcpy(buff, s); /* copy string to buffer */
buff[pdot - s] = lua_getlocaledecpoint(); /* correct decimal point */
endptr = l_str2dloc(buff, result, mode); /* try again */
if (endptr != NULL)
endptr = s + (endptr - buff); /* make relative to 's' */
}
return endptr;
}
#define MAXBY10 cast(lua_Unsigned, LUA_MAXINTEGER / 10)
#define MAXLASTD cast_int(LUA_MAXINTEGER % 10)
static const char *l_str2int (const char *s, lua_Integer *result) {
lua_Unsigned a = 0;
int empty = 1;
int neg;
while (lisspace(cast_uchar(*s))) s++; /* skip initial spaces */
neg = isneg(&s);
if (s[0] == '0' &&
(s[1] == 'x' || s[1] == 'X')) { /* hex? */
s += 2; /* skip '0x' */
for (; lisxdigit(cast_uchar(*s)); s++) {
a = a * 16 + luaO_hexavalue(*s);
empty = 0;
}
}
else { /* decimal */
for (; lisdigit(cast_uchar(*s)); s++) {
int d = *s - '0';
if (a >= MAXBY10 && (a > MAXBY10 || d > MAXLASTD + neg)) /* overflow? */
return NULL; /* do not accept it (as integer) */
a = a * 10 + d;
empty = 0;
}
}
while (lisspace(cast_uchar(*s))) s++; /* skip trailing spaces */
if (empty || *s != '\0') return NULL; /* something wrong in the numeral */
else {
*result = l_castU2S((neg) ? 0u - a : a);
return s;
}
}
size_t luaO_str2num (const char *s, TValue *o) {
lua_Integer i; lua_Number n;
const char *e;
if ((e = l_str2int(s, &i)) != NULL) { /* try as an integer */
setivalue(o, i);
}
else if ((e = l_str2d(s, &n)) != NULL) { /* else try as a float */
setfltvalue(o, n);
}
else
return 0; /* conversion failed */
return (e - s) + 1; /* success; return string size */
}
int luaO_utf8esc (char *buff, unsigned long x) {
int n = 1; /* number of bytes put in buffer (backwards) */
lua_assert(x <= 0x7FFFFFFFu);
if (x < 0x80) /* ascii? */
buff[UTF8BUFFSZ - 1] = cast_char(x);
else { /* need continuation bytes */
unsigned int mfb = 0x3f; /* maximum that fits in first byte */
do { /* add continuation bytes */
buff[UTF8BUFFSZ - (n++)] = cast_char(0x80 | (x & 0x3f));
x >>= 6; /* remove added bits */
mfb >>= 1; /* now there is one less bit available in first byte */
} while (x > mfb); /* still needs continuation byte? */
buff[UTF8BUFFSZ - n] = cast_char((~mfb << 1) | x); /* add first byte */
}
return n;
}
/*
** Maximum length of the conversion of a number to a string. Must be
** enough to accommodate both LUA_INTEGER_FMT and LUA_NUMBER_FMT.
** (For a long long int, this is 19 digits plus a sign and a final '\0',
** adding to 21. For a long double, it can go to a sign, 33 digits,
** the dot, an exponent letter, an exponent sign, 5 exponent digits,
** and a final '\0', adding to 43.)
*/
#define MAXNUMBER2STR 44
/*
** Convert a number object to a string, adding it to a buffer
*/
static int tostringbuff (TValue *obj, char *buff) {
int len;
lua_assert(ttisnumber(obj));
if (ttisinteger(obj))
len = lua_integer2str(buff, MAXNUMBER2STR, ivalue(obj));
else {
len = lua_number2str(buff, MAXNUMBER2STR, fltvalue(obj));
if (buff[strspn(buff, "-0123456789")] == '\0') { /* looks like an int? */
buff[len++] = lua_getlocaledecpoint();
buff[len++] = '0'; /* adds '.0' to result */
}
}
return len;
}
/*
** Convert a number object to a Lua string, replacing the value at 'obj'
*/
void luaO_tostring (lua_State *L, TValue *obj) {
char buff[MAXNUMBER2STR];
int len = tostringbuff(obj, buff);
setsvalue(L, obj, luaS_newlstr(L, buff, len));
}
/*
** {==================================================================
** 'luaO_pushvfstring'
** ===================================================================
*/
/*
** Size for buffer space used by 'luaO_pushvfstring'. It should be
** (LUA_IDSIZE + MAXNUMBER2STR) + a minimal space for basic messages,
** so that 'luaG_addinfo' can work directly on the buffer.
*/
#define BUFVFS (LUA_IDSIZE + MAXNUMBER2STR + 95)
/* buffer used by 'luaO_pushvfstring' */
typedef struct BuffFS {
lua_State *L;
int pushed; /* true if there is a part of the result on the stack */
int blen; /* length of partial string in 'space' */
char space[BUFVFS]; /* holds last part of the result */
} BuffFS;
/*
** Push given string to the stack, as part of the result, and
** join it to previous partial result if there is one.
** It may call 'luaV_concat' while using one slot from EXTRA_STACK.
** This call cannot invoke metamethods, as both operands must be
** strings. It can, however, raise an error if the result is too
** long. In that case, 'luaV_concat' frees the extra slot before
** raising the error.
*/
static void pushstr (BuffFS *buff, const char *str, size_t lstr) {
lua_State *L = buff->L;
setsvalue2s(L, L->top.p, luaS_newlstr(L, str, lstr));
L->top.p++; /* may use one slot from EXTRA_STACK */
if (!buff->pushed) /* no previous string on the stack? */
buff->pushed = 1; /* now there is one */
else /* join previous string with new one */
luaV_concat(L, 2);
}
/*
** empty the buffer space into the stack
*/
static void clearbuff (BuffFS *buff) {
pushstr(buff, buff->space, buff->blen); /* push buffer contents */
buff->blen = 0; /* space now is empty */
}
/*
** Get a space of size 'sz' in the buffer. If buffer has not enough
** space, empty it. 'sz' must fit in an empty buffer.
*/
static char *getbuff (BuffFS *buff, int sz) {
lua_assert(buff->blen <= BUFVFS); lua_assert(sz <= BUFVFS);
if (sz > BUFVFS - buff->blen) /* not enough space? */
clearbuff(buff);
return buff->space + buff->blen;
}
#define addsize(b,sz) ((b)->blen += (sz))
/*
** Add 'str' to the buffer. If string is larger than the buffer space,
** push the string directly to the stack.
*/
static void addstr2buff (BuffFS *buff, const char *str, size_t slen) {
if (slen <= BUFVFS) { /* does string fit into buffer? */
char *bf = getbuff(buff, cast_int(slen));
memcpy(bf, str, slen); /* add string to buffer */
addsize(buff, cast_int(slen));
}
else { /* string larger than buffer */
clearbuff(buff); /* string comes after buffer's content */
pushstr(buff, str, slen); /* push string */
}
}
/*
** Add a numeral to the buffer.
*/
static void addnum2buff (BuffFS *buff, TValue *num) {
char *numbuff = getbuff(buff, MAXNUMBER2STR);
int len = tostringbuff(num, numbuff); /* format number into 'numbuff' */
addsize(buff, len);
}
/*
** this function handles only '%d', '%c', '%f', '%p', '%s', and '%%'
conventional formats, plus Lua-specific '%I' and '%U'
*/
const char *luaO_pushvfstring (lua_State *L, const char *fmt, va_list argp) {
BuffFS buff; /* holds last part of the result */
const char *e; /* points to next '%' */
buff.pushed = buff.blen = 0;
buff.L = L;
while ((e = strchr(fmt, '%')) != NULL) {
addstr2buff(&buff, fmt, e - fmt); /* add 'fmt' up to '%' */
switch (*(e + 1)) { /* conversion specifier */
case 's': { /* zero-terminated string */
const char *s = va_arg(argp, char *);
if (s == NULL) s = "(null)";
addstr2buff(&buff, s, strlen(s));
break;
}
case 'c': { /* an 'int' as a character */
char c = cast_uchar(va_arg(argp, int));
addstr2buff(&buff, &c, sizeof(char));
break;
}
case 'd': { /* an 'int' */
TValue num;
setivalue(&num, va_arg(argp, int));
addnum2buff(&buff, &num);
break;
}
case 'I': { /* a 'lua_Integer' */
TValue num;
setivalue(&num, cast(lua_Integer, va_arg(argp, l_uacInt)));
addnum2buff(&buff, &num);
break;
}
case 'f': { /* a 'lua_Number' */
TValue num;
setfltvalue(&num, cast_num(va_arg(argp, l_uacNumber)));
addnum2buff(&buff, &num);
break;
}
case 'p': { /* a pointer */
const int sz = 3 * sizeof(void*) + 8; /* enough space for '%p' */
char *bf = getbuff(&buff, sz);
void *p = va_arg(argp, void *);
int len = lua_pointer2str(bf, sz, p);
addsize(&buff, len);
break;
}
case 'U': { /* a 'long' as a UTF-8 sequence */
char bf[UTF8BUFFSZ];
int len = luaO_utf8esc(bf, va_arg(argp, long));
addstr2buff(&buff, bf + UTF8BUFFSZ - len, len);
break;
}
case '%': {
addstr2buff(&buff, "%", 1);
break;
}
default: {
luaG_runerror(L, "invalid option '%%%c' to 'lua_pushfstring'",
*(e + 1));
}
}
fmt = e + 2; /* skip '%' and the specifier */
}
addstr2buff(&buff, fmt, strlen(fmt)); /* rest of 'fmt' */
clearbuff(&buff); /* empty buffer into the stack */
lua_assert(buff.pushed == 1);
return getstr(tsvalue(s2v(L->top.p - 1)));
}
const char *luaO_pushfstring (lua_State *L, const char *fmt, ...) {
const char *msg;
va_list argp;
va_start(argp, fmt);
msg = luaO_pushvfstring(L, fmt, argp);
va_end(argp);
return msg;
}
/* }================================================================== */
#define RETS "..."
#define PRE "[string \""
#define POS "\"]"
#define addstr(a,b,l) ( memcpy(a,b,(l) * sizeof(char)), a += (l) )
void luaO_chunkid (char *out, const char *source, size_t srclen) {
size_t bufflen = LUA_IDSIZE; /* free space in buffer */
if (*source == '=') { /* 'literal' source */
if (srclen <= bufflen) /* small enough? */
memcpy(out, source + 1, srclen * sizeof(char));
else { /* truncate it */
addstr(out, source + 1, bufflen - 1);
*out = '\0';
}
}
else if (*source == '@') { /* file name */
if (srclen <= bufflen) /* small enough? */
memcpy(out, source + 1, srclen * sizeof(char));
else { /* add '...' before rest of name */
addstr(out, RETS, LL(RETS));
bufflen -= LL(RETS);
memcpy(out, source + 1 + srclen - bufflen, bufflen * sizeof(char));
}
}
else { /* string; format as [string "source"] */
const char *nl = strchr(source, '\n'); /* find first new line (if any) */
addstr(out, PRE, LL(PRE)); /* add prefix */
bufflen -= LL(PRE RETS POS) + 1; /* save space for prefix+suffix+'\0' */
if (srclen < bufflen && nl == NULL) { /* small one-line source? */
addstr(out, source, srclen); /* keep it */
}
else {
if (nl != NULL) srclen = nl - source; /* stop at first newline */
if (srclen > bufflen) srclen = bufflen;
addstr(out, source, srclen);
addstr(out, RETS, LL(RETS));
}
memcpy(out, POS, (LL(POS) + 1) * sizeof(char));
}
}

View File

@@ -0,0 +1,813 @@
/*
** $Id: lobject.h $
** Type definitions for Lua objects
** See Copyright Notice in lua.h
*/
#ifndef lobject_h
#define lobject_h
#include <stdarg.h>
#include "llimits.h"
#include "lua.h"
/*
** Extra types for collectable non-values
*/
#define LUA_TUPVAL LUA_NUMTYPES /* upvalues */
#define LUA_TPROTO (LUA_NUMTYPES+1) /* function prototypes */
#define LUA_TDEADKEY (LUA_NUMTYPES+2) /* removed keys in tables */
/*
** number of all possible types (including LUA_TNONE but excluding DEADKEY)
*/
#define LUA_TOTALTYPES (LUA_TPROTO + 2)
/*
** tags for Tagged Values have the following use of bits:
** bits 0-3: actual tag (a LUA_T* constant)
** bits 4-5: variant bits
** bit 6: whether value is collectable
*/
/* add variant bits to a type */
#define makevariant(t,v) ((t) | ((v) << 4))
/*
** Union of all Lua values
*/
typedef union Value {
struct GCObject *gc; /* collectable objects */
void *p; /* light userdata */
lua_CFunction f; /* light C functions */
lua_Integer i; /* integer numbers */
lua_Number n; /* float numbers */
/* not used, but may avoid warnings for uninitialized value */
lu_byte ub;
} Value;
/*
** Tagged Values. This is the basic representation of values in Lua:
** an actual value plus a tag with its type.
*/
#define TValuefields Value value_; lu_byte tt_
typedef struct TValue {
TValuefields;
} TValue;
#define val_(o) ((o)->value_)
#define valraw(o) (val_(o))
/* raw type tag of a TValue */
#define rawtt(o) ((o)->tt_)
/* tag with no variants (bits 0-3) */
#define novariant(t) ((t) & 0x0F)
/* type tag of a TValue (bits 0-3 for tags + variant bits 4-5) */
#define withvariant(t) ((t) & 0x3F)
#define ttypetag(o) withvariant(rawtt(o))
/* type of a TValue */
#define ttype(o) (novariant(rawtt(o)))
/* Macros to test type */
#define checktag(o,t) (rawtt(o) == (t))
#define checktype(o,t) (ttype(o) == (t))
/* Macros for internal tests */
/* collectable object has the same tag as the original value */
#define righttt(obj) (ttypetag(obj) == gcvalue(obj)->tt)
/*
** Any value being manipulated by the program either is non
** collectable, or the collectable object has the right tag
** and it is not dead. The option 'L == NULL' allows other
** macros using this one to be used where L is not available.
*/
#define checkliveness(L,obj) \
((void)L, lua_longassert(!iscollectable(obj) || \
(righttt(obj) && (L == NULL || !isdead(G(L),gcvalue(obj))))))
/* Macros to set values */
/* set a value's tag */
#define settt_(o,t) ((o)->tt_=(t))
/* main macro to copy values (from 'obj2' to 'obj1') */
#define setobj(L,obj1,obj2) \
{ TValue *io1=(obj1); const TValue *io2=(obj2); \
io1->value_ = io2->value_; settt_(io1, io2->tt_); \
checkliveness(L,io1); lua_assert(!isnonstrictnil(io1)); }
/*
** Different types of assignments, according to source and destination.
** (They are mostly equal now, but may be different in the future.)
*/
/* from stack to stack */
#define setobjs2s(L,o1,o2) setobj(L,s2v(o1),s2v(o2))
/* to stack (not from same stack) */
#define setobj2s(L,o1,o2) setobj(L,s2v(o1),o2)
/* from table to same table */
#define setobjt2t setobj
/* to new object */
#define setobj2n setobj
/* to table */
#define setobj2t setobj
/*
** Entries in a Lua stack. Field 'tbclist' forms a list of all
** to-be-closed variables active in this stack. Dummy entries are
** used when the distance between two tbc variables does not fit
** in an unsigned short. They are represented by delta==0, and
** their real delta is always the maximum value that fits in
** that field.
*/
typedef union StackValue {
TValue val;
struct {
TValuefields;
unsigned short delta;
} tbclist;
} StackValue;
/* index to stack elements */
typedef StackValue *StkId;
/*
** When reallocating the stack, change all pointers to the stack into
** proper offsets.
*/
typedef union {
StkId p; /* actual pointer */
ptrdiff_t offset; /* used while the stack is being reallocated */
} StkIdRel;
/* convert a 'StackValue' to a 'TValue' */
#define s2v(o) (&(o)->val)
/*
** {==================================================================
** Nil
** ===================================================================
*/
/* Standard nil */
#define LUA_VNIL makevariant(LUA_TNIL, 0)
/* Empty slot (which might be different from a slot containing nil) */
#define LUA_VEMPTY makevariant(LUA_TNIL, 1)
/* Value returned for a key not found in a table (absent key) */
#define LUA_VABSTKEY makevariant(LUA_TNIL, 2)
/* macro to test for (any kind of) nil */
#define ttisnil(v) checktype((v), LUA_TNIL)
/* macro to test for a standard nil */
#define ttisstrictnil(o) checktag((o), LUA_VNIL)
#define setnilvalue(obj) settt_(obj, LUA_VNIL)
#define isabstkey(v) checktag((v), LUA_VABSTKEY)
/*
** macro to detect non-standard nils (used only in assertions)
*/
#define isnonstrictnil(v) (ttisnil(v) && !ttisstrictnil(v))
/*
** By default, entries with any kind of nil are considered empty.
** (In any definition, values associated with absent keys must also
** be accepted as empty.)
*/
#define isempty(v) ttisnil(v)
/* macro defining a value corresponding to an absent key */
#define ABSTKEYCONSTANT {NULL}, LUA_VABSTKEY
/* mark an entry as empty */
#define setempty(v) settt_(v, LUA_VEMPTY)
/* }================================================================== */
/*
** {==================================================================
** Booleans
** ===================================================================
*/
#define LUA_VFALSE makevariant(LUA_TBOOLEAN, 0)
#define LUA_VTRUE makevariant(LUA_TBOOLEAN, 1)
#define ttisboolean(o) checktype((o), LUA_TBOOLEAN)
#define ttisfalse(o) checktag((o), LUA_VFALSE)
#define ttistrue(o) checktag((o), LUA_VTRUE)
#define l_isfalse(o) (ttisfalse(o) || ttisnil(o))
#define setbfvalue(obj) settt_(obj, LUA_VFALSE)
#define setbtvalue(obj) settt_(obj, LUA_VTRUE)
/* }================================================================== */
/*
** {==================================================================
** Threads
** ===================================================================
*/
#define LUA_VTHREAD makevariant(LUA_TTHREAD, 0)
#define ttisthread(o) checktag((o), ctb(LUA_VTHREAD))
#define thvalue(o) check_exp(ttisthread(o), gco2th(val_(o).gc))
#define setthvalue(L,obj,x) \
{ TValue *io = (obj); lua_State *x_ = (x); \
val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VTHREAD)); \
checkliveness(L,io); }
#define setthvalue2s(L,o,t) setthvalue(L,s2v(o),t)
/* }================================================================== */
/*
** {==================================================================
** Collectable Objects
** ===================================================================
*/
/*
** Common Header for all collectable objects (in macro form, to be
** included in other objects)
*/
#define CommonHeader struct GCObject *next; lu_byte tt; lu_byte marked
/* Common type for all collectable objects */
typedef struct GCObject {
CommonHeader;
} GCObject;
/* Bit mark for collectable types */
#define BIT_ISCOLLECTABLE (1 << 6)
#define iscollectable(o) (rawtt(o) & BIT_ISCOLLECTABLE)
/* mark a tag as collectable */
#define ctb(t) ((t) | BIT_ISCOLLECTABLE)
#define gcvalue(o) check_exp(iscollectable(o), val_(o).gc)
#define gcvalueraw(v) ((v).gc)
#define setgcovalue(L,obj,x) \
{ TValue *io = (obj); GCObject *i_g=(x); \
val_(io).gc = i_g; settt_(io, ctb(i_g->tt)); }
/* }================================================================== */
/*
** {==================================================================
** Numbers
** ===================================================================
*/
/* Variant tags for numbers */
#define LUA_VNUMINT makevariant(LUA_TNUMBER, 0) /* integer numbers */
#define LUA_VNUMFLT makevariant(LUA_TNUMBER, 1) /* float numbers */
#define ttisnumber(o) checktype((o), LUA_TNUMBER)
#define ttisfloat(o) checktag((o), LUA_VNUMFLT)
#define ttisinteger(o) checktag((o), LUA_VNUMINT)
#define nvalue(o) check_exp(ttisnumber(o), \
(ttisinteger(o) ? cast_num(ivalue(o)) : fltvalue(o)))
#define fltvalue(o) check_exp(ttisfloat(o), val_(o).n)
#define ivalue(o) check_exp(ttisinteger(o), val_(o).i)
#define fltvalueraw(v) ((v).n)
#define ivalueraw(v) ((v).i)
#define setfltvalue(obj,x) \
{ TValue *io=(obj); val_(io).n=(x); settt_(io, LUA_VNUMFLT); }
#define chgfltvalue(obj,x) \
{ TValue *io=(obj); lua_assert(ttisfloat(io)); val_(io).n=(x); }
#define setivalue(obj,x) \
{ TValue *io=(obj); val_(io).i=(x); settt_(io, LUA_VNUMINT); }
#define chgivalue(obj,x) \
{ TValue *io=(obj); lua_assert(ttisinteger(io)); val_(io).i=(x); }
/* }================================================================== */
/*
** {==================================================================
** Strings
** ===================================================================
*/
/* Variant tags for strings */
#define LUA_VSHRSTR makevariant(LUA_TSTRING, 0) /* short strings */
#define LUA_VLNGSTR makevariant(LUA_TSTRING, 1) /* long strings */
#define ttisstring(o) checktype((o), LUA_TSTRING)
#define ttisshrstring(o) checktag((o), ctb(LUA_VSHRSTR))
#define ttislngstring(o) checktag((o), ctb(LUA_VLNGSTR))
#define tsvalueraw(v) (gco2ts((v).gc))
#define tsvalue(o) check_exp(ttisstring(o), gco2ts(val_(o).gc))
#define setsvalue(L,obj,x) \
{ TValue *io = (obj); TString *x_ = (x); \
val_(io).gc = obj2gco(x_); settt_(io, ctb(x_->tt)); \
checkliveness(L,io); }
/* set a string to the stack */
#define setsvalue2s(L,o,s) setsvalue(L,s2v(o),s)
/* set a string to a new object */
#define setsvalue2n setsvalue
/*
** Header for a string value.
*/
typedef struct TString {
CommonHeader;
lu_byte extra; /* reserved words for short strings; "has hash" for longs */
lu_byte shrlen; /* length for short strings, 0xFF for long strings */
unsigned int hash;
union {
size_t lnglen; /* length for long strings */
struct TString *hnext; /* linked list for hash table */
} u;
char contents[1];
} TString;
/*
** Get the actual string (array of bytes) from a 'TString'. (Generic
** version and specialized versions for long and short strings.)
*/
#define getstr(ts) ((ts)->contents)
#define getlngstr(ts) check_exp((ts)->shrlen == 0xFF, (ts)->contents)
#define getshrstr(ts) check_exp((ts)->shrlen != 0xFF, (ts)->contents)
/* get string length from 'TString *s' */
#define tsslen(s) \
((s)->shrlen != 0xFF ? (s)->shrlen : (s)->u.lnglen)
/* }================================================================== */
/*
** {==================================================================
** Userdata
** ===================================================================
*/
/*
** Light userdata should be a variant of userdata, but for compatibility
** reasons they are also different types.
*/
#define LUA_VLIGHTUSERDATA makevariant(LUA_TLIGHTUSERDATA, 0)
#define LUA_VUSERDATA makevariant(LUA_TUSERDATA, 0)
#define ttislightuserdata(o) checktag((o), LUA_VLIGHTUSERDATA)
#define ttisfulluserdata(o) checktag((o), ctb(LUA_VUSERDATA))
#define pvalue(o) check_exp(ttislightuserdata(o), val_(o).p)
#define uvalue(o) check_exp(ttisfulluserdata(o), gco2u(val_(o).gc))
#define pvalueraw(v) ((v).p)
#define setpvalue(obj,x) \
{ TValue *io=(obj); val_(io).p=(x); settt_(io, LUA_VLIGHTUSERDATA); }
#define setuvalue(L,obj,x) \
{ TValue *io = (obj); Udata *x_ = (x); \
val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VUSERDATA)); \
checkliveness(L,io); }
/* Ensures that addresses after this type are always fully aligned. */
typedef union UValue {
TValue uv;
LUAI_MAXALIGN; /* ensures maximum alignment for udata bytes */
} UValue;
/*
** Header for userdata with user values;
** memory area follows the end of this structure.
*/
typedef struct Udata {
CommonHeader;
unsigned short nuvalue; /* number of user values */
size_t len; /* number of bytes */
struct Table *metatable;
GCObject *gclist;
UValue uv[1]; /* user values */
} Udata;
/*
** Header for userdata with no user values. These userdata do not need
** to be gray during GC, and therefore do not need a 'gclist' field.
** To simplify, the code always use 'Udata' for both kinds of userdata,
** making sure it never accesses 'gclist' on userdata with no user values.
** This structure here is used only to compute the correct size for
** this representation. (The 'bindata' field in its end ensures correct
** alignment for binary data following this header.)
*/
typedef struct Udata0 {
CommonHeader;
unsigned short nuvalue; /* number of user values */
size_t len; /* number of bytes */
struct Table *metatable;
union {LUAI_MAXALIGN;} bindata;
} Udata0;
/* compute the offset of the memory area of a userdata */
#define udatamemoffset(nuv) \
((nuv) == 0 ? offsetof(Udata0, bindata) \
: offsetof(Udata, uv) + (sizeof(UValue) * (nuv)))
/* get the address of the memory block inside 'Udata' */
#define getudatamem(u) (cast_charp(u) + udatamemoffset((u)->nuvalue))
/* compute the size of a userdata */
#define sizeudata(nuv,nb) (udatamemoffset(nuv) + (nb))
/* }================================================================== */
/*
** {==================================================================
** Prototypes
** ===================================================================
*/
#define LUA_VPROTO makevariant(LUA_TPROTO, 0)
/*
** Description of an upvalue for function prototypes
*/
typedef struct Upvaldesc {
TString *name; /* upvalue name (for debug information) */
lu_byte instack; /* whether it is in stack (register) */
lu_byte idx; /* index of upvalue (in stack or in outer function's list) */
lu_byte kind; /* kind of corresponding variable */
} Upvaldesc;
/*
** Description of a local variable for function prototypes
** (used for debug information)
*/
typedef struct LocVar {
TString *varname;
int startpc; /* first point where variable is active */
int endpc; /* first point where variable is dead */
} LocVar;
/*
** Associates the absolute line source for a given instruction ('pc').
** The array 'lineinfo' gives, for each instruction, the difference in
** lines from the previous instruction. When that difference does not
** fit into a byte, Lua saves the absolute line for that instruction.
** (Lua also saves the absolute line periodically, to speed up the
** computation of a line number: we can use binary search in the
** absolute-line array, but we must traverse the 'lineinfo' array
** linearly to compute a line.)
*/
typedef struct AbsLineInfo {
int pc;
int line;
} AbsLineInfo;
/*
** Function Prototypes
*/
typedef struct Proto {
CommonHeader;
lu_byte numparams; /* number of fixed (named) parameters */
lu_byte is_vararg;
lu_byte maxstacksize; /* number of registers needed by this function */
int sizeupvalues; /* size of 'upvalues' */
int sizek; /* size of 'k' */
int sizecode;
int sizelineinfo;
int sizep; /* size of 'p' */
int sizelocvars;
int sizeabslineinfo; /* size of 'abslineinfo' */
int linedefined; /* debug information */
int lastlinedefined; /* debug information */
TValue *k; /* constants used by the function */
Instruction *code; /* opcodes */
struct Proto **p; /* functions defined inside the function */
Upvaldesc *upvalues; /* upvalue information */
ls_byte *lineinfo; /* information about source lines (debug information) */
AbsLineInfo *abslineinfo; /* idem */
LocVar *locvars; /* information about local variables (debug information) */
TString *source; /* used for debug information */
GCObject *gclist;
} Proto;
/* }================================================================== */
/*
** {==================================================================
** Functions
** ===================================================================
*/
#define LUA_VUPVAL makevariant(LUA_TUPVAL, 0)
/* Variant tags for functions */
#define LUA_VLCL makevariant(LUA_TFUNCTION, 0) /* Lua closure */
#define LUA_VLCF makevariant(LUA_TFUNCTION, 1) /* light C function */
#define LUA_VCCL makevariant(LUA_TFUNCTION, 2) /* C closure */
#define ttisfunction(o) checktype(o, LUA_TFUNCTION)
#define ttisLclosure(o) checktag((o), ctb(LUA_VLCL))
#define ttislcf(o) checktag((o), LUA_VLCF)
#define ttisCclosure(o) checktag((o), ctb(LUA_VCCL))
#define ttisclosure(o) (ttisLclosure(o) || ttisCclosure(o))
#define isLfunction(o) ttisLclosure(o)
#define clvalue(o) check_exp(ttisclosure(o), gco2cl(val_(o).gc))
#define clLvalue(o) check_exp(ttisLclosure(o), gco2lcl(val_(o).gc))
#define fvalue(o) check_exp(ttislcf(o), val_(o).f)
#define clCvalue(o) check_exp(ttisCclosure(o), gco2ccl(val_(o).gc))
#define fvalueraw(v) ((v).f)
#define setclLvalue(L,obj,x) \
{ TValue *io = (obj); LClosure *x_ = (x); \
val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VLCL)); \
checkliveness(L,io); }
#define setclLvalue2s(L,o,cl) setclLvalue(L,s2v(o),cl)
#define setfvalue(obj,x) \
{ TValue *io=(obj); val_(io).f=(x); settt_(io, LUA_VLCF); }
#define setclCvalue(L,obj,x) \
{ TValue *io = (obj); CClosure *x_ = (x); \
val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VCCL)); \
checkliveness(L,io); }
/*
** Upvalues for Lua closures
*/
typedef struct UpVal {
CommonHeader;
union {
TValue *p; /* points to stack or to its own value */
ptrdiff_t offset; /* used while the stack is being reallocated */
} v;
union {
struct { /* (when open) */
struct UpVal *next; /* linked list */
struct UpVal **previous;
} open;
TValue value; /* the value (when closed) */
} u;
} UpVal;
#define ClosureHeader \
CommonHeader; lu_byte nupvalues; GCObject *gclist
typedef struct CClosure {
ClosureHeader;
lua_CFunction f;
TValue upvalue[1]; /* list of upvalues */
} CClosure;
typedef struct LClosure {
ClosureHeader;
struct Proto *p;
UpVal *upvals[1]; /* list of upvalues */
} LClosure;
typedef union Closure {
CClosure c;
LClosure l;
} Closure;
#define getproto(o) (clLvalue(o)->p)
/* }================================================================== */
/*
** {==================================================================
** Tables
** ===================================================================
*/
#define LUA_VTABLE makevariant(LUA_TTABLE, 0)
#define ttistable(o) checktag((o), ctb(LUA_VTABLE))
#define hvalue(o) check_exp(ttistable(o), gco2t(val_(o).gc))
#define sethvalue(L,obj,x) \
{ TValue *io = (obj); Table *x_ = (x); \
val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VTABLE)); \
checkliveness(L,io); }
#define sethvalue2s(L,o,h) sethvalue(L,s2v(o),h)
/*
** Nodes for Hash tables: A pack of two TValue's (key-value pairs)
** plus a 'next' field to link colliding entries. The distribution
** of the key's fields ('key_tt' and 'key_val') not forming a proper
** 'TValue' allows for a smaller size for 'Node' both in 4-byte
** and 8-byte alignments.
*/
typedef union Node {
struct NodeKey {
TValuefields; /* fields for value */
lu_byte key_tt; /* key type */
int next; /* for chaining */
Value key_val; /* key value */
} u;
TValue i_val; /* direct access to node's value as a proper 'TValue' */
} Node;
/* copy a value into a key */
#define setnodekey(L,node,obj) \
{ Node *n_=(node); const TValue *io_=(obj); \
n_->u.key_val = io_->value_; n_->u.key_tt = io_->tt_; \
checkliveness(L,io_); }
/* copy a value from a key */
#define getnodekey(L,obj,node) \
{ TValue *io_=(obj); const Node *n_=(node); \
io_->value_ = n_->u.key_val; io_->tt_ = n_->u.key_tt; \
checkliveness(L,io_); }
/*
** About 'alimit': if 'isrealasize(t)' is true, then 'alimit' is the
** real size of 'array'. Otherwise, the real size of 'array' is the
** smallest power of two not smaller than 'alimit' (or zero iff 'alimit'
** is zero); 'alimit' is then used as a hint for #t.
*/
#define BITRAS (1 << 7)
#define isrealasize(t) (!((t)->flags & BITRAS))
#define setrealasize(t) ((t)->flags &= cast_byte(~BITRAS))
#define setnorealasize(t) ((t)->flags |= BITRAS)
typedef struct Table {
CommonHeader;
lu_byte flags; /* 1<<p means tagmethod(p) is not present */
lu_byte lsizenode; /* log2 of size of 'node' array */
unsigned int alimit; /* "limit" of 'array' array */
TValue *array; /* array part */
Node *node;
Node *lastfree; /* any free position is before this position */
struct Table *metatable;
GCObject *gclist;
} Table;
/*
** Macros to manipulate keys inserted in nodes
*/
#define keytt(node) ((node)->u.key_tt)
#define keyval(node) ((node)->u.key_val)
#define keyisnil(node) (keytt(node) == LUA_TNIL)
#define keyisinteger(node) (keytt(node) == LUA_VNUMINT)
#define keyival(node) (keyval(node).i)
#define keyisshrstr(node) (keytt(node) == ctb(LUA_VSHRSTR))
#define keystrval(node) (gco2ts(keyval(node).gc))
#define setnilkey(node) (keytt(node) = LUA_TNIL)
#define keyiscollectable(n) (keytt(n) & BIT_ISCOLLECTABLE)
#define gckey(n) (keyval(n).gc)
#define gckeyN(n) (keyiscollectable(n) ? gckey(n) : NULL)
/*
** Dead keys in tables have the tag DEADKEY but keep their original
** gcvalue. This distinguishes them from regular keys but allows them to
** be found when searched in a special way. ('next' needs that to find
** keys removed from a table during a traversal.)
*/
#define setdeadkey(node) (keytt(node) = LUA_TDEADKEY)
#define keyisdead(node) (keytt(node) == LUA_TDEADKEY)
/* }================================================================== */
/*
** 'module' operation for hashing (size is always a power of 2)
*/
#define lmod(s,size) \
(check_exp((size&(size-1))==0, (cast_int((s) & ((size)-1)))))
#define twoto(x) (1<<(x))
#define sizenode(t) (twoto((t)->lsizenode))
/* size of buffer for 'luaO_utf8esc' function */
#define UTF8BUFFSZ 8
LUAI_FUNC int luaO_utf8esc (char *buff, unsigned long x);
LUAI_FUNC int luaO_ceillog2 (unsigned int x);
LUAI_FUNC int luaO_rawarith (lua_State *L, int op, const TValue *p1,
const TValue *p2, TValue *res);
LUAI_FUNC void luaO_arith (lua_State *L, int op, const TValue *p1,
const TValue *p2, StkId res);
LUAI_FUNC size_t luaO_str2num (const char *s, TValue *o);
LUAI_FUNC int luaO_hexavalue (int c);
LUAI_FUNC void luaO_tostring (lua_State *L, TValue *obj);
LUAI_FUNC const char *luaO_pushvfstring (lua_State *L, const char *fmt,
va_list argp);
LUAI_FUNC const char *luaO_pushfstring (lua_State *L, const char *fmt, ...);
LUAI_FUNC void luaO_chunkid (char *out, const char *source, size_t srclen);
#endif

View File

@@ -0,0 +1,104 @@
/*
** $Id: lopcodes.c $
** Opcodes for Lua virtual machine
** See Copyright Notice in lua.h
*/
#define lopcodes_c
#define LUA_CORE
#include "lprefix.h"
#include "lopcodes.h"
/* ORDER OP */
LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = {
/* MM OT IT T A mode opcode */
opmode(0, 0, 0, 0, 1, iABC) /* OP_MOVE */
,opmode(0, 0, 0, 0, 1, iAsBx) /* OP_LOADI */
,opmode(0, 0, 0, 0, 1, iAsBx) /* OP_LOADF */
,opmode(0, 0, 0, 0, 1, iABx) /* OP_LOADK */
,opmode(0, 0, 0, 0, 1, iABx) /* OP_LOADKX */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_LOADFALSE */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_LFALSESKIP */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_LOADTRUE */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_LOADNIL */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_GETUPVAL */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_SETUPVAL */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_GETTABUP */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_GETTABLE */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_GETI */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_GETFIELD */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_SETTABUP */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_SETTABLE */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_SETI */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_SETFIELD */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_NEWTABLE */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SELF */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_ADDI */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_ADDK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SUBK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_MULK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_MODK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_POWK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_DIVK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_IDIVK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BANDK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BORK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BXORK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SHRI */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SHLI */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_ADD */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SUB */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_MUL */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_MOD */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_POW */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_DIV */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_IDIV */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BAND */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BOR */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BXOR */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SHL */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SHR */
,opmode(1, 0, 0, 0, 0, iABC) /* OP_MMBIN */
,opmode(1, 0, 0, 0, 0, iABC) /* OP_MMBINI*/
,opmode(1, 0, 0, 0, 0, iABC) /* OP_MMBINK*/
,opmode(0, 0, 0, 0, 1, iABC) /* OP_UNM */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BNOT */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_NOT */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_LEN */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_CONCAT */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_CLOSE */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_TBC */
,opmode(0, 0, 0, 0, 0, isJ) /* OP_JMP */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_EQ */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_LT */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_LE */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_EQK */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_EQI */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_LTI */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_LEI */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_GTI */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_GEI */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_TEST */
,opmode(0, 0, 0, 1, 1, iABC) /* OP_TESTSET */
,opmode(0, 1, 1, 0, 1, iABC) /* OP_CALL */
,opmode(0, 1, 1, 0, 1, iABC) /* OP_TAILCALL */
,opmode(0, 0, 1, 0, 0, iABC) /* OP_RETURN */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_RETURN0 */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_RETURN1 */
,opmode(0, 0, 0, 0, 1, iABx) /* OP_FORLOOP */
,opmode(0, 0, 0, 0, 1, iABx) /* OP_FORPREP */
,opmode(0, 0, 0, 0, 0, iABx) /* OP_TFORPREP */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_TFORCALL */
,opmode(0, 0, 0, 0, 1, iABx) /* OP_TFORLOOP */
,opmode(0, 0, 1, 0, 0, iABC) /* OP_SETLIST */
,opmode(0, 0, 0, 0, 1, iABx) /* OP_CLOSURE */
,opmode(0, 1, 0, 0, 1, iABC) /* OP_VARARG */
,opmode(0, 0, 1, 0, 1, iABC) /* OP_VARARGPREP */
,opmode(0, 0, 0, 0, 0, iAx) /* OP_EXTRAARG */
};

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