Skip to content

Commit

Permalink
fix: reduce rendered frames in frameloop "demand"
Browse files Browse the repository at this point in the history
  • Loading branch information
bbohlender committed May 10, 2024
1 parent d6fce21 commit 52b0b60
Show file tree
Hide file tree
Showing 10 changed files with 90 additions and 30 deletions.
2 changes: 1 addition & 1 deletion examples/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"r3f-perf": "^7.1.2",
"react-dom": "^18.2.0",
"vite-plugin-mkcert": "^1.17.4",
"zustand": "4"
"zustand": "^4.4.7"
},
"scripts": {
"dev": "vite --host",
Expand Down
71 changes: 52 additions & 19 deletions examples/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Canvas } from '@react-three/fiber'
import { Canvas, useFrame } from '@react-three/fiber'
import { Container, Fullscreen, Text, setPreferredColorScheme } from '@react-three/uikit'
import { Activity, CreditCard, DollarSign, Users } from '@react-three/uikit-lucide'

Expand All @@ -15,31 +15,64 @@ import { Overview } from './components/Overview.js'
import { RecentSales } from './components/RecentSales.js'
import { TeamSwitcher } from './components/TeamSwitcher.js'
import { UserNav } from './components/UserNav.js'
import { create } from 'zustand'

setPreferredColorScheme('light')

const useFrameCounter = create(() => 0)

export default function App() {
const [open, setOpen] = useState(false)
return (
<Canvas
events={noEvents}
frameloop="demand"
flat
camera={{ position: [0, 0, 18], fov: 35 }}
style={{ height: '100dvh', touchAction: 'none' }}
gl={{ localClippingEnabled: true }}
<>
<FrameCounter />
<Canvas
events={noEvents}
frameloop="demand"
flat
camera={{ position: [0, 0, 18], fov: 35 }}
style={{ height: '100dvh', touchAction: 'none' }}
gl={{ localClippingEnabled: true }}
>
<CountFrames />
<XWebPointers />
<Fullscreen distanceToCamera={1} backgroundColor={0xffffff} dark={{ backgroundColor: 0x0 }}>
<Defaults>
<DialogAnchor>
<Container flexDirection="column" width="100%" height="100%" overflow="scroll">
<DashboardPage open={open} setOpen={setOpen} />
</Container>
</DialogAnchor>
</Defaults>
</Fullscreen>
</Canvas>
</>
)
}

function CountFrames() {
useFrame(() => useFrameCounter.setState(useFrameCounter.getState() + 1))
return null
}

function FrameCounter() {
const counter = useFrameCounter()
return (
<div
style={{
position: 'absolute',
top: 0,
right: 0,
backgroundColor: 'black',
fontSize: '2rem',
padding: '0.5rem 1rem',
color: 'white',
fontFamily: 'sans-serif',
zIndex: 100,
}}
>
<XWebPointers />
<Fullscreen distanceToCamera={1} backgroundColor={0xffffff} dark={{ backgroundColor: 0x0 }}>
<Defaults>
<DialogAnchor>
<Container flexDirection="column" width="100%" height="100%" overflow="scroll">
<DashboardPage open={open} setOpen={setOpen} />
</Container>
</DialogAnchor>
</Defaults>
</Fullscreen>
</Canvas>
{counter}
</div>
)
}

Expand Down
2 changes: 1 addition & 1 deletion examples/dashboard/src/components/MainNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { colors } from '@/theme.js'
export function MainNav(props: Omit<ComponentPropsWithoutRef<typeof Container>, 'children'>) {
return (
<Container alignItems="center" flexDirection="row" gap={16} lg={{ gap: 24 }} {...props}>
<Text fontSize={14} lineHeight={20} fontWeight="medium" hover={{ color: colors.primary }}>
<Text fontSize={14} lineHeight={20} fontWeight="medium">
Overview
</Text>
<Text color={colors.mutedForeground} fontSize={14} lineHeight={20} fontWeight="medium">
Expand Down
16 changes: 15 additions & 1 deletion packages/react/src/root.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { invalidate, useFrame, useStore, useThree } from '@react-three/fiber'
import { addAfterEffect, addEffect, invalidate, useFrame, useStore, useThree } from '@react-three/fiber'
import { EventHandlers } from '@react-three/fiber/dist/declarations/src/core/events'
import { forwardRef, ReactNode, RefAttributes, useEffect, useMemo, useRef } from 'react'
import { ParentProvider } from './context.js'
Expand All @@ -23,6 +23,11 @@ export type RootProperties = BaseRootProperties &
children?: ReactNode
} & EventHandlers

let isRendering = false

addEffect(() => (isRendering = true))
addAfterEffect(() => (isRendering = false))

export const Root: (props: RootProperties & RefAttributes<ComponentInternals<RootProperties>>) => ReactNode =
forwardRef((properties, ref) => {
const renderer = useThree((state) => state.gl)
Expand All @@ -46,6 +51,15 @@ export const Root: (props: RootProperties & RefAttributes<ComponentInternals<Roo
() => store.getState().camera,
renderer,
onFrameSet,
() => {
if (isRendering) {
//request render unnecassary -> already rendering
return
}
//not rendering -> requesting a new frame
invalidate()
},
//requestFrame = invalidate, because invalidate always causes another frame
invalidate,
),
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
5 changes: 4 additions & 1 deletion packages/uikit/src/components/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function createRoot(
renderer: WebGLRenderer,
onFrameSet: Set<(delta: number) => void>,
requestRender: () => void = () => {},
requestFrame: () => void = () => {},
) {
const rootSize = signal<Vector2Tuple>([0, 0])
const hoveredSignal = signal<Array<number>>([])
Expand All @@ -106,10 +107,11 @@ export function createRoot(
const renderOrder = computedInheritableProperty(mergedProperties, 'renderOrder', 0)
const depthTest = computedInheritableProperty(mergedProperties, 'depthTest', true)

const ctx: WithCameraDistance & Pick<RootContext, 'requestRender' | 'onFrameSet' | 'pixelSize'> = {
const ctx: WithCameraDistance & Pick<RootContext, 'requestFrame' | 'requestRender' | 'onFrameSet' | 'pixelSize'> = {
cameraDistance: 0,
onFrameSet,
requestRender,
requestFrame,
pixelSize,
}

Expand Down Expand Up @@ -198,6 +200,7 @@ export function createRoot(
const gylphGroupManager = new GlyphGroupManager(renderOrder, depthTest, pixelSize, ctx, object, initializers)

const rootCtx: RootContext = Object.assign(ctx, {
requestFrame,
scrollPosition,
requestCalculateLayout,
cameraDistance: 0,
Expand Down
1 change: 1 addition & 0 deletions packages/uikit/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ export type RootContext = WithCameraDistance &
renderer: WebGLRenderer
size: Signal<Vector2Tuple | undefined>
requestRender: () => void
requestFrame: () => void
}>
15 changes: 9 additions & 6 deletions packages/uikit/src/scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function computedScrollHandlers(
{ scrollable, maxScrollPosition }: FlexNodeState,
object: Object3DRef,
listeners: Signal<ScrollListeners | undefined>,
root: Pick<RootContext, 'onFrameSet' | 'requestRender' | 'pixelSize'>,
root: Pick<RootContext, 'onFrameSet' | 'requestRender' | 'pixelSize' | 'requestFrame'>,
initializers: Initializers,
) {
const isScrollable = computed(() => scrollable.value?.some((scrollable) => scrollable) ?? false)
Expand Down Expand Up @@ -143,16 +143,16 @@ export function computedScrollHandlers(

scrollVelocity.multiplyScalar(0.9) //damping scroll factor

if (Math.abs(scrollVelocity.x) < 0.01) {
if (Math.abs(scrollVelocity.x) < 10 /** px per second */) {
scrollVelocity.x = 0
} else {
root.requestRender()
root.requestFrame()
}

if (Math.abs(scrollVelocity.y) < 0.01) {
if (Math.abs(scrollVelocity.y) < 10 /** px per second */) {
scrollVelocity.y = 0
} else {
root.requestRender()
root.requestFrame()
}

if (deltaX === 0 && deltaY === 0) {
Expand All @@ -176,7 +176,10 @@ export function computedScrollHandlers(
return undefined
}
const onPointerFinish = ({ nativeEvent }: ThreeEvent<PointerEvent>) => {
downPointerMap.delete(nativeEvent.pointerId)
if (!downPointerMap.delete(nativeEvent.pointerId) || downPointerMap.size > 0 || scrollPosition.value == null) {
return
}
//only request a render if the last pointer that was dragging stopped dragging and this panel is actually scrollable
root.requestRender()
}
return {
Expand Down
2 changes: 2 additions & 0 deletions packages/uikit/src/vanilla/fullscreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class Fullscreen extends Root {
properties?: FullscreenProperties,
defaultProperties?: AllOptionalProperties,
fontFamilies?: FontFamilies,
requestRender?: () => void,
) {
const sizeX = signal(0)
const sizeY = signal(0)
Expand All @@ -32,6 +33,7 @@ export class Fullscreen extends Root {
{ ...properties, sizeX, sizeY, pixelSize, transformTranslateZ },
defaultProperties,
fontFamilies,
requestRender,
)
this.matrixAutoUpdate = false
this.parentCameraSignal = parentCameraSignal
Expand Down
4 changes: 4 additions & 0 deletions packages/uikit/src/vanilla/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export class Root extends Parent {
properties?: RootProperties & WithReactive<{ pixelSize?: number }>,
defaultProperties?: AllOptionalProperties,
fontFamilies?: FontFamilies,
requestRender?: () => void,
requestFrame?: () => void,
) {
super()
this.pixelSizeSignal = signal(properties?.pixelSize ?? DEFAULT_PIXEL_SIZE)
Expand Down Expand Up @@ -51,6 +53,8 @@ export class Root extends Parent {
getCamera,
renderer,
this.onFrameSet,
requestRender,
requestFrame,
)
this.mergedProperties = internals.mergedProperties
this.contextSignal.value = Object.assign(internals, { fontFamiliesSignal: this.fontFamiliesSignal })
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 52b0b60

Please sign in to comment.