WGC capture fixes + high-resolution timer pacing for all loops
- Fix WGC capture_frame() returning stale frames (80k "frames" in 2s) by tracking new-frame events; return None when no new frame arrived - Add draw_border config passthrough with Win11 22H2+ platform check - Add high_resolution_timer() utility (timeBeginPeriod/EndPeriod) - Switch all processing loops from time.time() to time.perf_counter() - Wrap all loops with high_resolution_timer() for ~1ms sleep precision - Add animation speed badges to static/gradient color strip cards Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,14 +43,22 @@ class WGCCaptureStream(CaptureStream):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
capture_cursor = self.config.get("capture_cursor", False)
|
capture_cursor = self.config.get("capture_cursor", False)
|
||||||
|
draw_border = self.config.get("draw_border", None)
|
||||||
|
|
||||||
# WGC uses 1-based monitor indexing
|
# WGC uses 1-based monitor indexing
|
||||||
wgc_monitor_index = self.display_index + 1
|
wgc_monitor_index = self.display_index + 1
|
||||||
|
|
||||||
self._capture_instance = self._wgc.WindowsCapture(
|
# draw_border toggling requires Windows 11 22H2+ (build 22621+).
|
||||||
|
# On older builds, passing any value crashes the capture session,
|
||||||
|
# so we only pass it when the platform supports it.
|
||||||
|
wgc_kwargs = dict(
|
||||||
cursor_capture=capture_cursor,
|
cursor_capture=capture_cursor,
|
||||||
monitor_index=wgc_monitor_index,
|
monitor_index=wgc_monitor_index,
|
||||||
)
|
)
|
||||||
|
if draw_border is not None and self._supports_border_toggle():
|
||||||
|
wgc_kwargs["draw_border"] = draw_border
|
||||||
|
|
||||||
|
self._capture_instance = self._wgc.WindowsCapture(**wgc_kwargs)
|
||||||
|
|
||||||
def on_frame_arrived(frame, capture_control):
|
def on_frame_arrived(frame, capture_control):
|
||||||
try:
|
try:
|
||||||
@@ -101,6 +109,16 @@ class WGCCaptureStream(CaptureStream):
|
|||||||
logger.error(f"Failed to initialize WGC for display {self.display_index}: {e}", exc_info=True)
|
logger.error(f"Failed to initialize WGC for display {self.display_index}: {e}", exc_info=True)
|
||||||
raise RuntimeError(f"Failed to initialize WGC for display {self.display_index}: {e}")
|
raise RuntimeError(f"Failed to initialize WGC for display {self.display_index}: {e}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _supports_border_toggle() -> bool:
|
||||||
|
"""Check if the platform supports WGC border toggle (Windows 11 22H2+, build 22621+)."""
|
||||||
|
try:
|
||||||
|
import platform
|
||||||
|
build = int(platform.version().split(".")[2])
|
||||||
|
return build >= 22621
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def _cleanup_internal(self) -> None:
|
def _cleanup_internal(self) -> None:
|
||||||
"""Internal cleanup helper."""
|
"""Internal cleanup helper."""
|
||||||
if self._capture_control:
|
if self._capture_control:
|
||||||
@@ -137,12 +155,15 @@ class WGCCaptureStream(CaptureStream):
|
|||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Only return a frame when the callback has delivered a new one
|
||||||
|
if not self._frame_event.is_set():
|
||||||
|
return None
|
||||||
|
|
||||||
with self._frame_lock:
|
with self._frame_lock:
|
||||||
if self._latest_frame is None:
|
if self._latest_frame is None:
|
||||||
raise RuntimeError(
|
return None
|
||||||
f"No frame available yet for display {self.display_index}."
|
|
||||||
)
|
|
||||||
frame = self._latest_frame
|
frame = self._latest_frame
|
||||||
|
self._frame_event.clear()
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"WGC captured display {self.display_index}: "
|
f"WGC captured display {self.display_index}: "
|
||||||
@@ -208,7 +229,7 @@ class WGCEngine(CaptureEngine):
|
|||||||
def get_default_config(cls) -> Dict[str, Any]:
|
def get_default_config(cls) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"capture_cursor": False,
|
"capture_cursor": False,
|
||||||
"draw_border": False,
|
"draw_border": None, # None = OS default; False = hide border (Win 11 22H2+ only)
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from wled_controller.core.capture.calibration import CalibrationConfig, PixelMap
|
|||||||
from wled_controller.core.capture.screen_capture import extract_border_pixels
|
from wled_controller.core.capture.screen_capture import extract_border_pixels
|
||||||
from wled_controller.core.processing.live_stream import LiveStream
|
from wled_controller.core.processing.live_stream import LiveStream
|
||||||
from wled_controller.utils import get_logger
|
from wled_controller.utils import get_logger
|
||||||
|
from wled_controller.utils.timer import high_resolution_timer
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -259,6 +260,7 @@ class PictureColorStripStream(ColorStripStream):
|
|||||||
"""Background thread: poll source, process, cache colors."""
|
"""Background thread: poll source, process, cache colors."""
|
||||||
cached_frame = None
|
cached_frame = None
|
||||||
|
|
||||||
|
with high_resolution_timer():
|
||||||
while self._running:
|
while self._running:
|
||||||
loop_start = time.perf_counter()
|
loop_start = time.perf_counter()
|
||||||
fps = self._fps
|
fps = self._fps
|
||||||
@@ -537,8 +539,9 @@ class StaticColorStripStream(ColorStripStream):
|
|||||||
def _animate_loop(self) -> None:
|
def _animate_loop(self) -> None:
|
||||||
"""Background thread: compute animated colors at ~30 fps when animation is active."""
|
"""Background thread: compute animated colors at ~30 fps when animation is active."""
|
||||||
frame_time = 1.0 / 30
|
frame_time = 1.0 / 30
|
||||||
|
with high_resolution_timer():
|
||||||
while self._running:
|
while self._running:
|
||||||
loop_start = time.time()
|
loop_start = time.perf_counter()
|
||||||
anim = self._animation
|
anim = self._animation
|
||||||
if anim and anim.get("enabled"):
|
if anim and anim.get("enabled"):
|
||||||
speed = float(anim.get("speed", 1.0))
|
speed = float(anim.get("speed", 1.0))
|
||||||
@@ -557,7 +560,7 @@ class StaticColorStripStream(ColorStripStream):
|
|||||||
with self._colors_lock:
|
with self._colors_lock:
|
||||||
self._colors = colors
|
self._colors = colors
|
||||||
|
|
||||||
elapsed = time.time() - loop_start
|
elapsed = time.perf_counter() - loop_start
|
||||||
time.sleep(max(frame_time - elapsed, 0.001))
|
time.sleep(max(frame_time - elapsed, 0.001))
|
||||||
|
|
||||||
|
|
||||||
@@ -651,8 +654,9 @@ class ColorCycleColorStripStream(ColorStripStream):
|
|||||||
def _animate_loop(self) -> None:
|
def _animate_loop(self) -> None:
|
||||||
"""Background thread: interpolate between colors at ~30 fps."""
|
"""Background thread: interpolate between colors at ~30 fps."""
|
||||||
frame_time = 1.0 / 30
|
frame_time = 1.0 / 30
|
||||||
|
with high_resolution_timer():
|
||||||
while self._running:
|
while self._running:
|
||||||
loop_start = time.time()
|
loop_start = time.perf_counter()
|
||||||
color_list = self._color_list
|
color_list = self._color_list
|
||||||
speed = self._cycle_speed
|
speed = self._cycle_speed
|
||||||
n = self._led_count
|
n = self._led_count
|
||||||
@@ -669,7 +673,7 @@ class ColorCycleColorStripStream(ColorStripStream):
|
|||||||
led_colors = np.tile(pixel, (n, 1))
|
led_colors = np.tile(pixel, (n, 1))
|
||||||
with self._colors_lock:
|
with self._colors_lock:
|
||||||
self._colors = led_colors
|
self._colors = led_colors
|
||||||
elapsed = time.time() - loop_start
|
elapsed = time.perf_counter() - loop_start
|
||||||
time.sleep(max(frame_time - elapsed, 0.001))
|
time.sleep(max(frame_time - elapsed, 0.001))
|
||||||
|
|
||||||
|
|
||||||
@@ -765,8 +769,9 @@ class GradientColorStripStream(ColorStripStream):
|
|||||||
_cached_base: Optional[np.ndarray] = None
|
_cached_base: Optional[np.ndarray] = None
|
||||||
_cached_n: int = 0
|
_cached_n: int = 0
|
||||||
_cached_stops: Optional[list] = None
|
_cached_stops: Optional[list] = None
|
||||||
|
with high_resolution_timer():
|
||||||
while self._running:
|
while self._running:
|
||||||
loop_start = time.time()
|
loop_start = time.perf_counter()
|
||||||
anim = self._animation
|
anim = self._animation
|
||||||
if anim and anim.get("enabled"):
|
if anim and anim.get("enabled"):
|
||||||
speed = float(anim.get("speed", 1.0))
|
speed = float(anim.get("speed", 1.0))
|
||||||
@@ -807,5 +812,5 @@ class GradientColorStripStream(ColorStripStream):
|
|||||||
with self._colors_lock:
|
with self._colors_lock:
|
||||||
self._colors = colors
|
self._colors = colors
|
||||||
|
|
||||||
elapsed = time.time() - loop_start
|
elapsed = time.perf_counter() - loop_start
|
||||||
time.sleep(max(frame_time - elapsed, 0.001))
|
time.sleep(max(frame_time - elapsed, 0.001))
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from wled_controller.core.processing.target_processor import (
|
|||||||
TargetProcessor,
|
TargetProcessor,
|
||||||
)
|
)
|
||||||
from wled_controller.utils import get_logger
|
from wled_controller.utils import get_logger
|
||||||
|
from wled_controller.utils.timer import high_resolution_timer
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -270,7 +271,7 @@ class KCTargetProcessor(TargetProcessor):
|
|||||||
frame_time = 1.0 / target_fps
|
frame_time = 1.0 / target_fps
|
||||||
fps_samples: collections.deque = collections.deque(maxlen=10)
|
fps_samples: collections.deque = collections.deque(maxlen=10)
|
||||||
timing_samples: collections.deque = collections.deque(maxlen=10)
|
timing_samples: collections.deque = collections.deque(maxlen=10)
|
||||||
prev_frame_time_stamp = time.time()
|
prev_frame_time_stamp = time.perf_counter()
|
||||||
prev_capture = None
|
prev_capture = None
|
||||||
last_broadcast_time = 0.0
|
last_broadcast_time = 0.0
|
||||||
send_timestamps: collections.deque = collections.deque()
|
send_timestamps: collections.deque = collections.deque()
|
||||||
@@ -299,8 +300,9 @@ class KCTargetProcessor(TargetProcessor):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
with high_resolution_timer():
|
||||||
while self._is_running:
|
while self._is_running:
|
||||||
loop_start = time.time()
|
loop_start = time.perf_counter()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
capture = self._live_stream.get_latest_frame()
|
capture = self._live_stream.get_latest_frame()
|
||||||
@@ -314,11 +316,11 @@ class KCTargetProcessor(TargetProcessor):
|
|||||||
# Keepalive: re-broadcast last colors
|
# Keepalive: re-broadcast last colors
|
||||||
if self._latest_colors and (loop_start - last_broadcast_time) >= 1.0:
|
if self._latest_colors and (loop_start - last_broadcast_time) >= 1.0:
|
||||||
await self._broadcast_colors(self._latest_colors)
|
await self._broadcast_colors(self._latest_colors)
|
||||||
last_broadcast_time = time.time()
|
last_broadcast_time = time.perf_counter()
|
||||||
send_timestamps.append(last_broadcast_time)
|
send_timestamps.append(last_broadcast_time)
|
||||||
self._metrics.frames_keepalive += 1
|
self._metrics.frames_keepalive += 1
|
||||||
self._metrics.frames_skipped += 1
|
self._metrics.frames_skipped += 1
|
||||||
now_ts = time.time()
|
now_ts = time.perf_counter()
|
||||||
while send_timestamps and send_timestamps[0] < now_ts - 1.0:
|
while send_timestamps and send_timestamps[0] < now_ts - 1.0:
|
||||||
send_timestamps.popleft()
|
send_timestamps.popleft()
|
||||||
self._metrics.fps_current = len(send_timestamps)
|
self._metrics.fps_current = len(send_timestamps)
|
||||||
@@ -345,7 +347,7 @@ class KCTargetProcessor(TargetProcessor):
|
|||||||
t_broadcast_start = time.perf_counter()
|
t_broadcast_start = time.perf_counter()
|
||||||
await self._broadcast_colors(colors)
|
await self._broadcast_colors(colors)
|
||||||
broadcast_ms = (time.perf_counter() - t_broadcast_start) * 1000
|
broadcast_ms = (time.perf_counter() - t_broadcast_start) * 1000
|
||||||
last_broadcast_time = time.time()
|
last_broadcast_time = time.perf_counter()
|
||||||
send_timestamps.append(last_broadcast_time)
|
send_timestamps.append(last_broadcast_time)
|
||||||
|
|
||||||
# Per-stage timing (rolling average over last 10 frames)
|
# Per-stage timing (rolling average over last 10 frames)
|
||||||
@@ -362,7 +364,7 @@ class KCTargetProcessor(TargetProcessor):
|
|||||||
self._metrics.last_update = datetime.utcnow()
|
self._metrics.last_update = datetime.utcnow()
|
||||||
|
|
||||||
# Calculate actual FPS
|
# Calculate actual FPS
|
||||||
now = time.time()
|
now = time.perf_counter()
|
||||||
interval = now - prev_frame_time_stamp
|
interval = now - prev_frame_time_stamp
|
||||||
prev_frame_time_stamp = now
|
prev_frame_time_stamp = now
|
||||||
fps_samples.append(1.0 / interval if interval > 0 else 0)
|
fps_samples.append(1.0 / interval if interval > 0 else 0)
|
||||||
@@ -383,7 +385,7 @@ class KCTargetProcessor(TargetProcessor):
|
|||||||
logger.error(f"KC processing error for {self._target_id}: {e}", exc_info=True)
|
logger.error(f"KC processing error for {self._target_id}: {e}", exc_info=True)
|
||||||
|
|
||||||
# Throttle to target FPS
|
# Throttle to target FPS
|
||||||
elapsed = time.time() - loop_start
|
elapsed = time.perf_counter() - loop_start
|
||||||
remaining = frame_time - elapsed
|
remaining = frame_time - elapsed
|
||||||
if remaining > 0:
|
if remaining > 0:
|
||||||
await asyncio.sleep(remaining)
|
await asyncio.sleep(remaining)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import numpy as np
|
|||||||
from wled_controller.core.capture_engines.base import CaptureStream, ScreenCapture
|
from wled_controller.core.capture_engines.base import CaptureStream, ScreenCapture
|
||||||
from wled_controller.core.filters import ImagePool, PostprocessingFilter
|
from wled_controller.core.filters import ImagePool, PostprocessingFilter
|
||||||
from wled_controller.utils import get_logger
|
from wled_controller.utils import get_logger
|
||||||
|
from wled_controller.utils.timer import high_resolution_timer
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -128,8 +129,9 @@ class ScreenCaptureLiveStream(LiveStream):
|
|||||||
|
|
||||||
def _capture_loop(self) -> None:
|
def _capture_loop(self) -> None:
|
||||||
frame_time = 1.0 / self._fps if self._fps > 0 else 1.0
|
frame_time = 1.0 / self._fps if self._fps > 0 else 1.0
|
||||||
|
with high_resolution_timer():
|
||||||
while self._running:
|
while self._running:
|
||||||
loop_start = time.time()
|
loop_start = time.perf_counter()
|
||||||
try:
|
try:
|
||||||
frame = self._capture_stream.capture_frame()
|
frame = self._capture_stream.capture_frame()
|
||||||
if frame is not None:
|
if frame is not None:
|
||||||
@@ -142,7 +144,7 @@ class ScreenCaptureLiveStream(LiveStream):
|
|||||||
logger.error(f"Capture error (display={self._capture_stream.display_index}): {e}")
|
logger.error(f"Capture error (display={self._capture_stream.display_index}): {e}")
|
||||||
|
|
||||||
# Throttle to target FPS
|
# Throttle to target FPS
|
||||||
elapsed = time.time() - loop_start
|
elapsed = time.perf_counter() - loop_start
|
||||||
remaining = frame_time - elapsed
|
remaining = frame_time - elapsed
|
||||||
if remaining > 0:
|
if remaining > 0:
|
||||||
time.sleep(remaining)
|
time.sleep(remaining)
|
||||||
@@ -224,8 +226,9 @@ class ProcessedLiveStream(LiveStream):
|
|||||||
fps = self.target_fps
|
fps = self.target_fps
|
||||||
frame_time = 1.0 / fps if fps > 0 else 1.0
|
frame_time = 1.0 / fps if fps > 0 else 1.0
|
||||||
|
|
||||||
|
with high_resolution_timer():
|
||||||
while self._running:
|
while self._running:
|
||||||
loop_start = time.time()
|
loop_start = time.perf_counter()
|
||||||
|
|
||||||
source_frame = self._source.get_latest_frame()
|
source_frame = self._source.get_latest_frame()
|
||||||
if source_frame is None or source_frame is cached_source_frame:
|
if source_frame is None or source_frame is cached_source_frame:
|
||||||
@@ -260,7 +263,7 @@ class ProcessedLiveStream(LiveStream):
|
|||||||
with self._frame_lock:
|
with self._frame_lock:
|
||||||
self._latest_frame = processed
|
self._latest_frame = processed
|
||||||
|
|
||||||
elapsed = time.time() - loop_start
|
elapsed = time.perf_counter() - loop_start
|
||||||
remaining = frame_time - elapsed
|
remaining = frame_time - elapsed
|
||||||
time.sleep(max(remaining, 0.001))
|
time.sleep(max(remaining, 0.001))
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from wled_controller.core.processing.target_processor import (
|
|||||||
TargetProcessor,
|
TargetProcessor,
|
||||||
)
|
)
|
||||||
from wled_controller.utils import get_logger
|
from wled_controller.utils import get_logger
|
||||||
|
from wled_controller.utils.timer import high_resolution_timer
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -367,7 +368,7 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
send_timestamps: collections.deque = collections.deque()
|
send_timestamps: collections.deque = collections.deque()
|
||||||
prev_colors = None
|
prev_colors = None
|
||||||
last_send_time = 0.0
|
last_send_time = 0.0
|
||||||
prev_frame_time_stamp = time.time()
|
prev_frame_time_stamp = time.perf_counter()
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -376,8 +377,9 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
with high_resolution_timer():
|
||||||
while self._is_running:
|
while self._is_running:
|
||||||
loop_start = now = time.time()
|
loop_start = now = time.perf_counter()
|
||||||
# Re-read target_fps each tick so hot-updates to the CSS source take effect
|
# Re-read target_fps each tick so hot-updates to the CSS source take effect
|
||||||
target_fps = stream.target_fps if stream.target_fps > 0 else 30
|
target_fps = stream.target_fps if stream.target_fps > 0 else 30
|
||||||
frame_time = 1.0 / target_fps
|
frame_time = 1.0 / target_fps
|
||||||
@@ -409,7 +411,7 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
self._led_client.send_pixels_fast(send_colors)
|
self._led_client.send_pixels_fast(send_colors)
|
||||||
else:
|
else:
|
||||||
await self._led_client.send_pixels(send_colors)
|
await self._led_client.send_pixels(send_colors)
|
||||||
now = time.time()
|
now = time.perf_counter()
|
||||||
last_send_time = now
|
last_send_time = now
|
||||||
send_timestamps.append(now)
|
send_timestamps.append(now)
|
||||||
self._metrics.frames_keepalive += 1
|
self._metrics.frames_keepalive += 1
|
||||||
@@ -435,7 +437,7 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
await self._led_client.send_pixels(send_colors)
|
await self._led_client.send_pixels(send_colors)
|
||||||
send_ms = (time.perf_counter() - t_send_start) * 1000
|
send_ms = (time.perf_counter() - t_send_start) * 1000
|
||||||
|
|
||||||
now = time.time()
|
now = time.perf_counter()
|
||||||
last_send_time = now
|
last_send_time = now
|
||||||
send_timestamps.append(now)
|
send_timestamps.append(now)
|
||||||
|
|
||||||
|
|||||||
@@ -172,8 +172,10 @@ export function createColorStripCard(source, pictureSourceMap) {
|
|||||||
const isGradient = source.source_type === 'gradient';
|
const isGradient = source.source_type === 'gradient';
|
||||||
const isColorCycle = source.source_type === 'color_cycle';
|
const isColorCycle = source.source_type === 'color_cycle';
|
||||||
|
|
||||||
const animBadge = ((isStatic || isGradient) && source.animation && source.animation.enabled)
|
const anim = (isStatic || isGradient) && source.animation && source.animation.enabled ? source.animation : null;
|
||||||
? `<span class="stream-card-prop" title="${t('color_strip.animation')}">✨ ${t('color_strip.animation.type.' + source.animation.type) || source.animation.type}</span>`
|
const animBadge = anim
|
||||||
|
? `<span class="stream-card-prop" title="${t('color_strip.animation')}">✨ ${t('color_strip.animation.type.' + anim.type) || anim.type}</span>`
|
||||||
|
+ `<span class="stream-card-prop" title="${t('color_strip.animation.speed')}">⏩ ${(anim.speed || 1.0).toFixed(1)}×</span>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
let propsHtml;
|
let propsHtml;
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
|
|
||||||
from .logger import setup_logging, get_logger
|
from .logger import setup_logging, get_logger
|
||||||
from .monitor_names import get_monitor_names, get_monitor_name, get_monitor_refresh_rates
|
from .monitor_names import get_monitor_names, get_monitor_name, get_monitor_refresh_rates
|
||||||
|
from .timer import high_resolution_timer
|
||||||
|
|
||||||
__all__ = ["setup_logging", "get_logger", "get_monitor_names", "get_monitor_name", "get_monitor_refresh_rates"]
|
__all__ = ["setup_logging", "get_logger", "get_monitor_names", "get_monitor_name", "get_monitor_refresh_rates", "high_resolution_timer"]
|
||||||
|
|||||||
34
server/src/wled_controller/utils/timer.py
Normal file
34
server/src/wled_controller/utils/timer.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""High-resolution timer utilities for precise sleep on Windows.
|
||||||
|
|
||||||
|
Windows default timer resolution is ~15.6ms, making time.sleep() very
|
||||||
|
imprecise for real-time loops (e.g. 60fps needs 16.67ms per frame).
|
||||||
|
|
||||||
|
Calling timeBeginPeriod(1) increases system timer resolution to 1ms,
|
||||||
|
making time.sleep() accurate to ~1ms. The calls are reference-counted
|
||||||
|
by Windows — each timeBeginPeriod must be paired with timeEndPeriod.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def high_resolution_timer():
|
||||||
|
"""Context manager that enables 1ms timer resolution on Windows.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
with high_resolution_timer():
|
||||||
|
while running:
|
||||||
|
...
|
||||||
|
time.sleep(remaining) # now accurate to ~1ms
|
||||||
|
"""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
import ctypes
|
||||||
|
ctypes.windll.winmm.timeBeginPeriod(1)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
if sys.platform == "win32":
|
||||||
|
import ctypes
|
||||||
|
ctypes.windll.winmm.timeEndPeriod(1)
|
||||||
Reference in New Issue
Block a user