Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
41990a4
fix(codegen): user class/fn names shadow prelude shims (Transform/Dup…
MarcosBrendonDePaula Jul 22, 2026
7b3727f
fix(codegen): module-level const/let read from a function resolves (g…
MarcosBrendonDePaula Jul 22, 2026
351e9ec
feat(rts-egui): pipeline 3D wgpu (scene pass) + 5 primitivos egui.mes…
MarcosBrendonDePaula Jul 23, 2026
a129b40
perf(rts-egui): religa backface culling (cull_mode Back) no scene pas…
MarcosBrendonDePaula Jul 23, 2026
5a7fb63
feat(rts-egui): specular Blinn-Phong no shader 3D (brilho nas superfi…
MarcosBrendonDePaula Jul 23, 2026
6d360e5
feat(rts-egui): skybox no scene pass 3D (gradiente + estrelas procedu…
MarcosBrendonDePaula Jul 23, 2026
4c324d2
feat(rts-egui): luz PONTUAL + flag emissivo por objeto no shader 3D
MarcosBrendonDePaula Jul 23, 2026
f0eb61c
feat(rts-egui): shadow map direcional (PCF) + textura procedural no s…
MarcosBrendonDePaula Jul 23, 2026
701a25a
feat(rts-egui): egui.winWidth/winHeight — tamanho lógico vivo da janela
MarcosBrendonDePaula Jul 23, 2026
287befe
feat(net): tcp_set_nonblocking + sentinela -2 (WouldBlock) no tcp_recv
MarcosBrendonDePaula Jul 23, 2026
ee259c8
fix(rts-egui): scene pass sem backface culling (cull_mode None)
MarcosBrendonDePaula Jul 23, 2026
af57caa
fix(codegen): let/const aninhado em __rtsn_main não aliasa mais a gce…
MarcosBrendonDePaula Jul 23, 2026
c16b8d3
chore(rts-egui): rebuild (bin) — sem mudanca de codigo (debug de diag…
MarcosBrendonDePaula Jul 23, 2026
ed4ac05
Merge remote-tracking branch 'origin/main' into feat/rts-egui-3d-scen…
MarcosBrendonDePaula Jul 23, 2026
d322e49
feat(rts-egui): enxerta o bom do gpu3d (origin/main) no scene pass — …
MarcosBrendonDePaula Jul 23, 2026
46602a9
docs(gpu3d): marca gpu3d-scene-pass.md como superseded — API viva é e…
MarcosBrendonDePaula Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added bin/rts.exe
Binary file not shown.
26 changes: 13 additions & 13 deletions crates/rts-codegen-new/src/front/run/funcval/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,14 @@ pub fn module_globals(
// written-free test can't fire on it), so plain user `const`s are unaffected.
let mut top: Vec<String> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
// Top-level bindings whose initializer is a NON-LITERAL (a call/member/etc. that
// yields a RUNTIME value with identity — e.g. `const c = atomic.i64_new(0)`). A
// literal init (`let p = "hi-"`) is re-materializable / by-value-capturable, so a
// read-only closure capture handles it WITHOUT a cell; a runtime-value init read
// from a plain function cannot be re-materialized and MUST be a shared cell.
let mut runtime_init: HashSet<String> = HashSet::new();
for s in &main.body {
let (name, init) = match s {
HirStmt::Let { name, init, .. } => (name, init.as_ref()),
HirStmt::Const { name, init, .. } => (name, Some(init)),
let name = match s {
HirStmt::Let { name, .. } => name,
HirStmt::Const { name, .. } => name,
_ => continue,
};
if seen.insert(name.clone()) {
top.push(name.clone());
if init.is_some_and(|e| !matches!(e.kind, HirExprKind::Lit(_))) {
runtime_init.insert(name.clone());
}
}
}
if top.is_empty() {
Expand Down Expand Up @@ -127,9 +118,18 @@ pub fn module_globals(
// it is READ free from a function AND its init is a runtime value (the
// read-only-shared-handle case, `const c = atomic.i64_new(0)`). A read-only
// LITERAL is left to the by-value closure-capture path (don't disturb it).
// Promote a top-level `let`/`const` to a runtime CELL when it is WRITTEN free
// from a function (#195 mutable case), force-promoted, OR simply READ free from
// a function. The read case now covers LITERAL inits too (`const NEAR = 0.05`
// used inside a plain top-level `function`): a plain function has no closure env
// to capture into, so the by-value capture path bailed ("not a simple local").
// A shared cell is also the JS-correct semantics (closures see later mutations),
// and a never-written literal const lands in `immutable_gcells` → read once at
// entry, so the cell cost is amortized. Top-level-only consts (never read from a
// function) stay off this path — plain user consts are unaffected.
let promote = written_free.contains(&name)
|| force.contains(&name)
|| (read_free.contains(&name) && runtime_init.contains(&name));
|| read_free.contains(&name);
if promote {
map.insert(name, id);
id += 1;
Expand Down
23 changes: 23 additions & 0 deletions crates/rts-codegen-new/src/front/run/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,14 @@ pub(crate) struct Lowerer<'a, 'b, 'c> {
/// inside ANY function declares a fresh LOCAL that shadows the global
/// (without this a prelude-internal `let out` clobbered a user gcell).
pub is_main: bool,
/// Lexical block nesting depth while lowering (0 = not yet in the body;
/// `lower_block` bumps it, so the function's OUTERMOST body statements are at
/// depth 1 and anything inside a loop/if/try/block is ≥2). In `__rtsn_main`
/// the promoted-global GCELL divert of a `let`/`const` must apply ONLY to the
/// top-level declaration (depth 1) — a same-spelled `let` NESTED in a loop is
/// a fresh block-scoped local that must SHADOW the global, not alias its cell
/// (otherwise a loop-body `const x` corrupts a top-level `x` across scopes).
pub block_depth: u32,
/// The NAME of the function being lowered (`__rtsn_main`, `__rtsn_ctor_C`,
/// `__rtsn_method_C_m`, `__rtsn_static_C_m`, …). Drives the TS access-surface
/// re-checks: a `readonly` field write is allowed only inside a
Expand Down Expand Up @@ -468,6 +476,7 @@ impl<'a, 'b, 'c> Lowerer<'a, 'b, 'c> {
classes,
is_prelude,
is_main: func.name == "__rtsn_main",
block_depth: 0,
current_fn: func.name.clone(),
builtins,
float_promoted: super::floatscan::float_promoted_locals(&func.body),
Expand Down Expand Up @@ -714,6 +723,20 @@ impl<'a, 'b, 'c> Lowerer<'a, 'b, 'c> {
&mut self,
module: &mut dyn Module,
stmts: &[HirStmt],
) -> FrontResult<()> {
// Track lexical nesting: the function body's own `lower_block` makes its
// direct statements depth 1; a nested loop/if/try/block body is ≥2. Used by
// `lower_let` to only divert a TOP-LEVEL (depth-1) main `let` to its gcell.
self.block_depth += 1;
let r = self.lower_block_inner(module, stmts);
self.block_depth -= 1;
r
}

fn lower_block_inner(
&mut self,
module: &mut dyn Module,
stmts: &[HirStmt],
) -> FrontResult<()> {
for s in stmts {
if self.block_terminated {
Expand Down
27 changes: 21 additions & 6 deletions crates/rts-codegen-new/src/front/run/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,17 +441,32 @@ fn merge_programs(
classes.insert(desc.clone());
}

// funcs: prelude first, then user — but the user SHADOWS the prelude by name.
// A user `class Transform` synthesizes `__rtsn_ctor_Transform` etc.; the prelude
// (node:stream) already defines a `Transform extends Duplex` under the SAME
// symbol. A blind append left BOTH ctors compiled: prune kept the prelude's
// (its name matched the user's referenced class) but dropped its now-unused
// `Duplex` super — so the prelude ctor compiled a call to a pruned function
// (`unknown function __rtsn_ctor_Duplex`). Dropping every prelude func the user
// redefines makes the user's definition win cleanly and lets prune reach the
// real dependency set. This is why user class/fn names no longer collide with
// embedded shims (Transform/Duplex/Readable/…).
let user_fn_names: std::collections::HashSet<String> =
user.funcs.iter().map(|f| f.name.clone()).collect();
let mut funcs: Vec<HirFunc> = prelude
.funcs
.into_iter()
.filter(|f| !user_fn_names.contains(&f.name))
.collect();

// PRIVACY GATE: record which function names came from the PRELUDE (the engine's
// embedded includes). The PRIVATE `engine` global is resolvable ONLY from these
// functions; a user function naming `engine.*` bails explicitly (see
// `engineobj`). The prelude's own classes/methods/arrows are all in
// `prelude.funcs`, so their names are exactly this set (the user's `__rtsn_main`
// and user functions are NOT included — `merge_programs` keeps `user.main`).
// `engineobj`). Computed over the SURVIVING prelude funcs (those the user did not
// shadow), so a user-redefined name is NOT granted prelude privacy.
let prelude_fns: std::collections::HashSet<String> =
prelude.funcs.iter().map(|f| f.name.clone()).collect();
funcs.iter().map(|f| f.name.clone()).collect();

// funcs: prelude first, then user (user appended; last-wins on name collision).
let mut funcs = prelude.funcs;
funcs.extend(user.funcs);

// fn_this_class + captures: prelude then user.
Expand Down
8 changes: 6 additions & 2 deletions crates/rts-codegen-new/src/front/run/stmt_let.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl<'a, 'b, 'c> Lowerer<'a, 'b, 'c> {
types::I64,
crate::value::PolyValue::undefined().raw() as i64,
);
if self.is_main {
if self.is_main && self.block_depth == 1 {
if let Some(id) = self.gcells.get(name).copied() {
self.emit_gcell_set(module, id, undef)?;
return Ok(());
Expand All @@ -59,7 +59,11 @@ impl<'a, 'b, 'c> Lowerer<'a, 'b, 'c> {
// fresh LOCAL that shadows the global (JS scoping) — routing it to the cell
// made the prelude's `Console.__format` local `let out` CLOBBER a user
// gcell named `out` (every console.log corrupted the captured variable).
if self.is_main {
// Only the TOP-LEVEL (depth-1) main declaration initializes the promoted
// gcell. A same-spelled `let`/`const` NESTED in a loop/if/block is a fresh
// block-scoped local that SHADOWS the global (JS scoping) — diverting it to
// the cell here made a loop-body `const x` alias and corrupt a top-level `x`.
if self.is_main && self.block_depth == 1 {
if let Some(id) = self.gcells.get(name).copied() {
let val = self.lower_expr(module, init)?;
let word = self.box_value(val);
Expand Down
50 changes: 22 additions & 28 deletions crates/rts-egui/src/frame/gpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,9 @@ pub struct RenderState {
/// Janela transparente → clear com alpha 0 (o painel egui também fica
/// transparente em `end_frame`), pra o SO compor o fundo.
pub transparent: bool,
/// Estado do scene pass 3D (`gpu3d`) desta janela — `None` até o 1º
/// `gpu3d.mesh`. Quando há draws no frame, a cena é gravada ANTES do pass
/// do egui (que então carrega em vez de limpar). Ver `crate::scene3d`.
pub scene: Option<crate::scene3d::SceneState>,
/// Pipeline 3D wgpu (scene pass) — `None` até o TS usar `egui.mesh*`/`drawMesh`.
/// Quando presente, roda ANTES do egui (limpa color+depth); o egui usa `Load`.
pub scene: Option<super::scene3d::Scene3D>,
}

impl RenderState {
Expand Down Expand Up @@ -354,39 +353,34 @@ pub(crate) fn present_wgpu(
r.renderer
.update_buffers(&r.device, &r.queue, &mut encoder, &paint_jobs, &screen_descriptor);

// ── Scene pass 3D (gpu3d) — ANTES do pass do egui, no MESMO encoder ─────
// Quando o TS enfileirou `gpu3d.draw` neste frame, a cena limpa cor+depth e
// desenha as malhas; o pass do egui abaixo então CARREGA (LoadOp::Load) em
// vez de limpar, compondo a UI por cima. Sem draws: comportamento idêntico
// ao anterior (egui limpa). Ver docs/specs/gpu3d-scene-pass.md.
let scene_drawn = crate::scene3d::record_if_active(
&mut r.scene,
&r.device,
&r.queue,
&mut encoder,
&view,
r.config.width,
r.config.height,
);
// ── SCENE PASS 3D (antes do egui, mesmo encoder) ─────────────────────────
// Se há um pipeline 3D ativo, ele limpa color+depth e desenha as meshes; o
// egui então compõe por cima com LoadOp::Load. Sem cena, o egui limpa (Clear),
// preservando o comportamento antigo das janelas puramente de UI.
let scene_cleared = if let Some(scene) = &mut r.scene {
scene.render(&r.device, &r.queue, &mut encoder, &view, r.config.width, r.config.height)
} else {
false
};

{
let egui_load = if scene_cleared {
wgpu::LoadOp::Load
} else {
wgpu::LoadOp::Clear(if r.transparent {
wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }
} else {
wgpu::Color { r: 0.02, g: 0.02, b: 0.03, a: 1.0 }
})
};
let pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("rts-egui pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
// Cena 3D desenhada → carrega (a UI compõe por cima).
// Transparente: clear com alpha 0 p/ o SO compor o fundo.
// Opaco: fundo escuro padrão.
load: if scene_drawn {
wgpu::LoadOp::Load
} else if r.transparent {
wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 })
} else {
wgpu::LoadOp::Clear(wgpu::Color { r: 0.02, g: 0.02, b: 0.03, a: 1.0 })
},
load: egui_load,
store: wgpu::StoreOp::Store,
},
})],
Expand Down
17 changes: 9 additions & 8 deletions crates/rts-egui/src/frame/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//! tesselará as shapes e renderiza/apresenta o frame via o backend ativo.

mod gpu;
pub(crate) mod scene3d;
mod render;

// API pública do módulo `frame` (mantém o `pub use frame::*` do lib.rs): os tipos
Expand Down Expand Up @@ -135,15 +136,15 @@ fn finish_frame(c: &mut crate::ctx::UiCtx, cmds: Vec<WidgetCmd>) {
// (`body { margin/padding }`), não do egui — senão há espaçamento duplo e
// o DOM não controla o layout (a "borda à esquerda" que aparecia era a
// `inner_margin` padrão do tema). O DOM/CSS é o dono do espaçamento.
// Transparente → sem fundo (Frame::NONE); opaco → fundo do tema mas margem 0.
// Cena 3D ativa (gpu3d.draw neste frame) → também SEM fundo: o painel
// opaco taparia o scene pass que o present grava por baixo do egui.
let scene_active = match &c.backend {
Backend::Wgpu(r) => r.scene.as_ref().is_some_and(|s| !s.draws.is_empty()),
#[cfg(feature = "glow-backend")]
Backend::Glow(_) => false,
// Transparente OU com CENA 3D ativa (scene pass já pintou o fundo/geometria)
// → sem fundo (Frame::NONE) pra o 3D aparecer; caso contrário opaco (fundo
// do tema, margem 0). Sem isto o CentralPanel taparia o scene pass.
let has_scene = if let crate::frame::Backend::Wgpu(r) = &c.backend {
r.scene.is_some()
} else {
false
};
let panel = if c.transparent || scene_active {
let panel = if c.transparent || has_scene {
egui::CentralPanel::default().frame(egui::Frame::NONE)
} else {
let bg = c.egui_ctx.style().visuals.panel_fill;
Expand Down
Loading
Loading