Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 16 additions & 1 deletion Editor/Sources/EditorApp/Panels/ViewportPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct ViewportPanel: View {
let shadowsEnabled = store.viewportShadowsEnabled
let renderScalePercent = store.viewportRenderScalePercent
let interactionDownscaleEnabled = store.viewportInteractionDownscaleEnabled
let realtimeEnabled = store.viewportRealtimeEnabled
let playbackState = store.playbackState

// 推送 gizmo 控制器所需的快照(摄像机 / 视口矩形 / 实体世界坐标)。
Expand Down Expand Up @@ -74,6 +75,7 @@ struct ViewportPanel: View {
shadowsEnabled: shadowsEnabled,
renderScalePercent: renderScalePercent,
interactionDownscaleEnabled: interactionDownscaleEnabled,
realtimeEnabled: realtimeEnabled,
playbackState: playbackState,
onSelectGizmoMode: { mode in
if gizmoMode != mode {
Expand All @@ -97,6 +99,9 @@ struct ViewportPanel: View {
onToggleInteractionDownscale: {
app.setViewportInteractionDownscaleEnabled(!interactionDownscaleEnabled)
},
onToggleRealtime: {
app.setViewportRealtimeEnabled(!realtimeEnabled)
},
onPlay: { app.applyPlaybackState(.playing) },
onPause: { app.applyPlaybackState(.paused) },
onStop: { app.applyPlaybackState(.stopped) })
Expand Down Expand Up @@ -1210,13 +1215,15 @@ private struct ViewportInfoBar: View {
let shadowsEnabled: Bool
let renderScalePercent: Int
let interactionDownscaleEnabled: Bool
let realtimeEnabled: Bool
let playbackState: PlaybackState
let onSelectGizmoMode: (EditorGizmoMode) -> Void
let onSelectGizmoSpace: (EditorGizmoSpace) -> Void
let onSelectShadingMode: (EditorViewportShadingMode) -> Void
let onToggleShadows: () -> Void
let onSelectRenderScale: (Int) -> Void
let onToggleInteractionDownscale: () -> Void
let onToggleRealtime: () -> Void
let onPlay: () -> Void
let onPause: () -> Void
let onStop: () -> Void
Expand Down Expand Up @@ -1272,8 +1279,10 @@ private struct ViewportInfoBar: View {

RenderScaleSelector(percent: renderScalePercent,
interactionDownscaleEnabled: interactionDownscaleEnabled,
realtimeEnabled: realtimeEnabled,
onSelect: onSelectRenderScale,
onToggleInteractionDownscale: onToggleInteractionDownscale)
onToggleInteractionDownscale: onToggleInteractionDownscale,
onToggleRealtime: onToggleRealtime)

Divider()
.frame(width: 1, height: 16)
Expand Down Expand Up @@ -1450,8 +1459,10 @@ private struct GizmoAxisChip: View {
private struct RenderScaleSelector: View {
let percent: Int
let interactionDownscaleEnabled: Bool
let realtimeEnabled: Bool
let onSelect: (Int) -> Void
let onToggleInteractionDownscale: () -> Void
let onToggleRealtime: () -> Void
@State private var isPresented: Bool = false

private static let presets = [50, 75, 100, 150, 200]
Expand Down Expand Up @@ -1486,6 +1497,10 @@ private struct RenderScaleSelector: View {
title: L("Downscale while navigating"),
isSelected: interactionDownscaleEnabled,
action: { onToggleInteractionDownscale() })))
entries.append(.item(MenuItem(id: "renderscale-realtime",
title: L("Realtime"),
isSelected: realtimeEnabled,
action: { onToggleRealtime() })))
return entries
}
}
Expand Down
5 changes: 4 additions & 1 deletion Editor/Sources/EditorApp/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ private func runLegacyEditor(launchOptions: EditorAppLaunchOptions) throws {
// instead of an off-palette dark blue.
clearColor: GPUColor(r: 0x1E / 255, g: 0x1F / 255, b: 0x22 / 255, a: 1),
backendConfig: launchOptions.backendConfig,
titleBarStyle: .hiddenInset),
titleBarStyle: .hiddenInset,
// 编辑器走事件驱动:空闲时 UI 与 3D 都不渲染(场景按需
// 渲染见 EditorViewportRenderGate),输入延迟与 GPU 负载解耦。
frameDrivePolicy: .eventDriven),
backend: backend,
events: events,
onTick: { dt in
Expand Down
70 changes: 70 additions & 0 deletions Editor/Sources/EditorCore/Editing/EditorViewportRenderGate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import RenderBackend
import SceneRuntime

/// Decides whether the engine should render the viewport this tick.
///
/// The signature samples the values that feed `RenderPacket` — a closed set:
/// if none changed, re-rendering would produce an identical image, so the
/// tick skips the GPU entirely (the editor stays on the last published
/// texture). Camera and palettes are compared by value, scene content by the
/// `RuntimeWorld` mutation revision, so there is no "remember to mark dirty"
/// discipline. A false-dirty merely degrades to today's continuous
/// rendering; a missed invalidation is caught by the heartbeat and by the
/// realtime toggle escape hatch.
public struct EditorViewportRenderGate {
public struct Signature: Equatable {
public var sceneRevision: UInt64
public var camera: RenderCamera
public var drawableSize: RenderDrawableSize
public var settingsGeneration: UInt64
public var jointPalettes: JointPaletteMap

public init(sceneRevision: UInt64,
camera: RenderCamera,
drawableSize: RenderDrawableSize,
settingsGeneration: UInt64,
jointPalettes: JointPaletteMap) {
self.sceneRevision = sceneRevision
self.camera = camera
self.drawableSize = drawableSize
self.settingsGeneration = settingsGeneration
self.jointPalettes = jointPalettes
}
}

/// Missed-invalidation safety net: render at least this often while the
/// editor ticks, so a forgotten dirty path shows up as a 1 Hz refresh
/// instead of a frozen viewport.
public static let heartbeatInterval: Double = 1.0
/// Frames rendered after the last change. Covers in-flight completion
/// handlers and double buffering.
public static let convergenceFrames = 2
/// TAA needs several frames after the last change for its history to
/// converge; stopping earlier would freeze a half-resolved image.
public static let temporalConvergenceFrames = 8

private var lastSignature: Signature?
private var lastRenderTime: Double = -.infinity
private var pendingFrames = 0

public init() {}

public mutating func shouldRender(signature: Signature,
forceContinuous: Bool,
hasViewportInput: Bool,
temporalEffectsActive: Bool,
now: Double) -> Bool {
if signature != lastSignature || forceContinuous || hasViewportInput {
pendingFrames = temporalEffectsActive
? Self.temporalConvergenceFrames
: Self.convergenceFrames
}
let render = pendingFrames > 0 || now - lastRenderTime >= Self.heartbeatInterval
if render {
lastSignature = signature
lastRenderTime = now
if pendingFrames > 0 { pendingFrames -= 1 }
}
return render
}
}
53 changes: 47 additions & 6 deletions Editor/Sources/EditorCore/EditorCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public final class EditorApplication: @unchecked Sendable {
private var pendingViewportEvents: [InputEvent] = []
private var _viewportDrawableSize: RenderDrawableSize = .init(width: 1280, height: 720)
private var lastViewportSurfaceState = ViewportSurfaceState()
private var renderGate = EditorViewportRenderGate()
private var renderSettingsGeneration: UInt64 = 0
private var lastQueuedRenderSettings = RenderSettings()
private var openSettingsWindowHandler: (() -> Void)?
private var displayInvalidationHandler: (() -> Void)?
private var vsyncModeHandler: ((EditorVSyncMode) -> Void)?
Expand Down Expand Up @@ -163,27 +166,46 @@ public final class EditorApplication: @unchecked Sendable {
engine.start(renderSurface: nil, enableViewportSurface: true)
// 默认启用离屏渲染,让引擎渲染到一个 viewport 纹理交给编辑器显示。
// 不开启 viewportResolve 时 UI 会一直停在 "Waiting for first render packet"。
engine.queueRenderSettings(makeViewportRenderSettings(
queueTrackedRenderSettings(makeViewportRenderSettings(
shadowsEnabled: store.state.viewportShadowsEnabled,
shadingMode: store.state.viewportShadingMode))
store.dispatch(.setConnected(true))
logConsole("Editor connected to runtime")
}

public func tick(deltaTime: Double) {
// Under the event-driven frame policy the loop can sleep for seconds;
// the wake-up tick must not step simulation/animation by the whole gap.
let deltaTime = min(max(deltaTime, 0), 0.25)
let didUpdateFrameTiming = recordFrameTiming(deltaTime)
store.dispatch(.tickFrame(store.state.frameIndex &+ 1))
let inputEvents = pendingViewportEvents
pendingViewportEvents.removeAll(keepingCapacity: true)
inputState.process(inputEvents)
scene.tickScene(deltaTime: deltaTime)
let drawableSize = effectiveViewportDrawableSize()
let jointPalettes = scene.currentJointPaletteMap()
let state = store.state
let renderViewport = renderGate.shouldRender(
signature: EditorViewportRenderGate.Signature(
sceneRevision: scene.revision,
camera: scene.currentRenderCamera(),
drawableSize: drawableSize,
settingsGeneration: renderSettingsGeneration,
jointPalettes: jointPalettes
),
forceContinuous: state.viewportRealtimeEnabled || state.playbackState == .playing,
hasViewportInput: !inputEvents.isEmpty,
temporalEffectsActive: lastQueuedRenderSettings.enableTAA,
now: monotonicNow()
)
engine.tick(
deltaTime: deltaTime,
inputEvents: inputEvents,
drawableSize: effectiveViewportDrawableSize(),
shouldRender: store.state.shouldRender,
drawableSize: drawableSize,
shouldRender: state.shouldRender && renderViewport,
renderSceneOverride: scene.currentRenderScene(),
jointPaletteOverride: scene.currentJointPaletteMap()
jointPaletteOverride: jointPalettes
)

let surface = engine.currentViewportSurfaceState()
Expand Down Expand Up @@ -248,6 +270,17 @@ public final class EditorApplication: @unchecked Sendable {
: "Viewport interaction downscale disabled")
}

public func setViewportRealtimeEnabled(_ enabled: Bool) {
guard store.state.viewportRealtimeEnabled != enabled else { return }
store.dispatch(.setViewportRealtime(enabled))
logConsole(enabled ? "Viewport realtime rendering enabled"
: "Viewport renders on demand")
}

private func monotonicNow() -> Double {
Double(DispatchTime.now().uptimeNanoseconds) / 1_000_000_000
}

public func setViewportRenderCompletionHandler(_ handler: (@Sendable (ViewportSurfaceState) -> Void)?) {
engine.setRenderCompletionHandler { completion in
handler?(completion.viewportSurfaceState)
Expand Down Expand Up @@ -517,14 +550,22 @@ public final class EditorApplication: @unchecked Sendable {
}

public func queueViewportRenderSettings(_ settings: RenderSettings) {
queueTrackedRenderSettings(settings)
}

/// Single funnel for render-settings changes: bumps the generation the
/// viewport render gate folds into its dirty signature.
private func queueTrackedRenderSettings(_ settings: RenderSettings) {
renderSettingsGeneration &+= 1
lastQueuedRenderSettings = settings
engine.queueRenderSettings(settings)
}

public func setViewportShadowsEnabled(_ enabled: Bool) {
if store.state.viewportShadowsEnabled != enabled {
store.dispatch(.setViewportShadowsEnabled(enabled))
}
engine.queueRenderSettings(makeViewportRenderSettings(
queueTrackedRenderSettings(makeViewportRenderSettings(
shadowsEnabled: enabled,
shadingMode: store.state.viewportShadingMode))
logConsole(enabled ? "Viewport shadows enabled" : "Viewport shadows disabled")
Expand All @@ -536,7 +577,7 @@ public final class EditorApplication: @unchecked Sendable {
if store.state.viewportShadingMode != mode {
store.dispatch(.setViewportShadingMode(mode))
}
engine.queueRenderSettings(makeViewportRenderSettings(
queueTrackedRenderSettings(makeViewportRenderSettings(
shadowsEnabled: store.state.viewportShadowsEnabled,
shadingMode: mode))
}
Expand Down
4 changes: 4 additions & 0 deletions Editor/Sources/EditorCore/State/EditorReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public enum EditorAction: Sendable {
case setViewportShadowsEnabled(Bool)
case setViewportRenderScalePercent(Int)
case setViewportInteractionDownscale(Bool)
case setViewportRealtime(Bool)
case setTranslateSnapEnabled(Bool)
case setRotateSnapEnabled(Bool)
case setScaleSnapEnabled(Bool)
Expand Down Expand Up @@ -126,6 +127,9 @@ public enum EditorReducer {
case let .setViewportInteractionDownscale(enabled):
state.viewportInteractionDownscaleEnabled = enabled

case let .setViewportRealtime(enabled):
state.viewportRealtimeEnabled = enabled

case let .setTranslateSnapEnabled(enabled):
state.translateSnapEnabled = enabled

Expand Down
8 changes: 8 additions & 0 deletions Editor/Sources/EditorCore/State/EditorState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ public struct EditorState: Codable, Sendable {
public var viewportRenderScalePercent: Int
/// Temporarily halve the render resolution during camera / gizmo drags.
public var viewportInteractionDownscaleEnabled: Bool
/// Escape hatch for on-demand viewport rendering: render every tick even
/// when no packet input changed (Unreal's per-viewport "Realtime").
public var viewportRealtimeEnabled: Bool
public var translateSnapEnabled: Bool
public var rotateSnapEnabled: Bool
public var scaleSnapEnabled: Bool
Expand Down Expand Up @@ -301,6 +304,7 @@ public struct EditorState: Codable, Sendable {
viewportShadowDebugMode: EditorViewportShadowDebugMode = .off,
viewportRenderScalePercent: Int = 100,
viewportInteractionDownscaleEnabled: Bool = false,
viewportRealtimeEnabled: Bool = false,
translateSnapEnabled: Bool = false,
rotateSnapEnabled: Bool = false,
scaleSnapEnabled: Bool = false,
Expand Down Expand Up @@ -346,6 +350,7 @@ public struct EditorState: Codable, Sendable {
self.viewportShadowDebugMode = viewportShadowDebugMode
self.viewportRenderScalePercent = Self.sanitizedRenderScalePercent(viewportRenderScalePercent)
self.viewportInteractionDownscaleEnabled = viewportInteractionDownscaleEnabled
self.viewportRealtimeEnabled = viewportRealtimeEnabled
self.translateSnapEnabled = translateSnapEnabled
self.rotateSnapEnabled = rotateSnapEnabled
self.scaleSnapEnabled = scaleSnapEnabled
Expand Down Expand Up @@ -409,6 +414,7 @@ public struct EditorState: Codable, Sendable {
case viewportShadowDebugMode
case viewportRenderScalePercent
case viewportInteractionDownscaleEnabled
case viewportRealtimeEnabled
case translateSnapEnabled
case rotateSnapEnabled
case scaleSnapEnabled
Expand Down Expand Up @@ -473,6 +479,7 @@ public struct EditorState: Codable, Sendable {
viewportShadowDebugMode: try c.decodeIfPresent(EditorViewportShadowDebugMode.self, forKey: .viewportShadowDebugMode) ?? .off,
viewportRenderScalePercent: try c.decodeIfPresent(Int.self, forKey: .viewportRenderScalePercent) ?? 100,
viewportInteractionDownscaleEnabled: try c.decodeIfPresent(Bool.self, forKey: .viewportInteractionDownscaleEnabled) ?? false,
viewportRealtimeEnabled: try c.decodeIfPresent(Bool.self, forKey: .viewportRealtimeEnabled) ?? false,
translateSnapEnabled: try c.decodeIfPresent(Bool.self, forKey: .translateSnapEnabled) ?? false,
rotateSnapEnabled: try c.decodeIfPresent(Bool.self, forKey: .rotateSnapEnabled) ?? false,
scaleSnapEnabled: try c.decodeIfPresent(Bool.self, forKey: .scaleSnapEnabled) ?? false,
Expand Down Expand Up @@ -519,6 +526,7 @@ public struct EditorState: Codable, Sendable {
try c.encode(viewportShadowDebugMode, forKey: .viewportShadowDebugMode)
try c.encode(viewportRenderScalePercent, forKey: .viewportRenderScalePercent)
try c.encode(viewportInteractionDownscaleEnabled, forKey: .viewportInteractionDownscaleEnabled)
try c.encode(viewportRealtimeEnabled, forKey: .viewportRealtimeEnabled)
try c.encode(translateSnapEnabled, forKey: .translateSnapEnabled)
try c.encode(rotateSnapEnabled, forKey: .rotateSnapEnabled)
try c.encode(scaleSnapEnabled, forKey: .scaleSnapEnabled)
Expand Down
6 changes: 6 additions & 0 deletions Editor/Sources/EditorCore/State/EditorStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public final class EditorStore: @unchecked Sendable {
case viewportShadowsEnabled
case viewportRenderScalePercent
case viewportInteractionDownscaleEnabled
case viewportRealtimeEnabled
case translateSnapEnabled
case rotateSnapEnabled
case scaleSnapEnabled
Expand Down Expand Up @@ -165,6 +166,8 @@ public final class EditorStore: @unchecked Sendable {
mark(.viewportInteractionDownscaleEnabled,
old.viewportInteractionDownscaleEnabled,
new.viewportInteractionDownscaleEnabled)
case .setViewportRealtime:
mark(.viewportRealtimeEnabled, old.viewportRealtimeEnabled, new.viewportRealtimeEnabled)
case .setTranslateSnapEnabled:
mark(.translateSnapEnabled, old.translateSnapEnabled, new.translateSnapEnabled)
case .setRotateSnapEnabled:
Expand Down Expand Up @@ -274,6 +277,9 @@ extension EditorStore {
public var viewportInteractionDownscaleEnabled: Bool {
read(.viewportInteractionDownscaleEnabled, storage.viewportInteractionDownscaleEnabled)
}
public var viewportRealtimeEnabled: Bool {
read(.viewportRealtimeEnabled, storage.viewportRealtimeEnabled)
}
public var translateSnapEnabled: Bool { read(.translateSnapEnabled, storage.translateSnapEnabled) }
public var rotateSnapEnabled: Bool { read(.rotateSnapEnabled, storage.rotateSnapEnabled) }
public var scaleSnapEnabled: Bool { read(.scaleSnapEnabled, storage.scaleSnapEnabled) }
Expand Down
Loading
Loading