Repository navigation
Fix Guitar Hero post-failure routing and cut gameplay vision latency in V11 #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
8d7a706
Add Guitar Hero V11 fast locked-highway vision
ProhibitedTV 6539fd8
Add Guitar Hero V11 route guard and gameplay fast path
ProhibitedTV db811fd
Activate Guitar Hero V11 profile
ProhibitedTV c602646
Tune Guitar Hero V11 for lower-latency vision
ProhibitedTV 15f8839
Export Guitar Hero V11 profile
ProhibitedTV 1aea071
Add Guitar Hero V11 runtime regressions
ProhibitedTV ac4025d
Update registry tests for Guitar Hero V11
ProhibitedTV 6b9b780
Relax V11 transition regression to the actual invariant
ProhibitedTV fdf3611
Assert V11 transition never regresses to boot
ProhibitedTV File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import numpy as np | ||
|
|
||
| from .guitar_hero_vision import LANE_NAMES | ||
| from .guitar_hero_vision_v10 import GuitarHeroVisionV10 | ||
|
|
||
|
|
||
| class GuitarHeroVisionV11(GuitarHeroVisionV10): | ||
| """V11 performance layer for the Guitar Hero highway tracker. | ||
|
|
||
| The V9 live bundle showed the runtime spending roughly 45-50 ms in policy code | ||
| while targeting a 16.7 ms loop. V8/V10 recompute a combinatorial five-lane | ||
| receptor layout every frame even after a stable highway has already been locked, | ||
| and they continue running menu morphology while actively playing. | ||
|
|
||
| V11 keeps the same geometry and temporal note semantics but adds two safe fast | ||
| paths: | ||
|
|
||
| * once a receptor layout is locked, validate it with tiny local color windows at | ||
| the known receptor centers instead of re-running the full candidate product; | ||
| * allow the active profile to suppress menu scoring during song launch/gameplay. | ||
|
|
||
| Any weak local support falls straight back to V8's full reacquisition path, so the | ||
| optimization cannot keep stale highway ownership across a results/menu screen. | ||
| """ | ||
|
|
||
| def __init__(self, cfg: dict) -> None: | ||
| super().__init__(cfg) | ||
| self.fast_lock_min_ratio = float(cfg.get("fast_lock_min_ratio", 0.035)) | ||
| self.fast_lock_half_width = float(cfg.get("fast_lock_half_width", 0.022)) | ||
| self.fast_lock_half_height = float(cfg.get("fast_lock_half_height", 0.026)) | ||
| self.skip_menu_scores = False | ||
| self.fast_layout_frames = 0 | ||
| self.full_layout_frames = 0 | ||
| self.menu_score_frames_skipped = 0 | ||
| self._fast_layout_used = False | ||
|
|
||
| def _locked_pixel_support(self, masks: dict[str, np.ndarray]) -> int: | ||
| centers = self._locked_centers | ||
| if centers is None: | ||
| return 0 | ||
| support = 0 | ||
| for lane_name, center in zip(LANE_NAMES, centers, strict=True): | ||
| mask = masks[lane_name] | ||
| h, w = mask.shape | ||
| cx = int(center[0] * w) | ||
| cy = int(center[1] * h) | ||
| xhalf = max(4, int(w * self.fast_lock_half_width)) | ||
| yhalf = max(4, int(h * self.fast_lock_half_height)) | ||
| x0 = max(0, cx - xhalf) | ||
| x1 = min(w, cx + xhalf + 1) | ||
| y0 = max(0, cy - yhalf) | ||
| y1 = min(h, cy + yhalf + 1) | ||
| roi = mask[y0:y1, x0:x1] | ||
| if roi.size == 0: | ||
| continue | ||
| ratio = float(np.count_nonzero(roi) / roi.size) | ||
| if ratio >= self.fast_lock_min_ratio: | ||
| support += 1 | ||
| return support | ||
|
|
||
| def _joint_layout(self, masks): | ||
| if self._locked_centers is not None: | ||
| support = self._locked_pixel_support(masks) | ||
| if support >= 3: | ||
| self._fast_layout_used = True | ||
| self.layout_locked = True | ||
| self.lock_support = support | ||
| self.fast_layout_frames += 1 | ||
| confidence = min(1.0, 0.64 + support * 0.07) | ||
| return self._locked_centers, confidence | ||
|
|
||
| self._fast_layout_used = False | ||
| self.full_layout_frames += 1 | ||
| return super()._joint_layout(masks) | ||
|
|
||
| def _lock_or_current(self, masks, centers, confidence): | ||
| if self._fast_layout_used and self._locked_centers is not None: | ||
| self.layout_locked = True | ||
| return self._locked_centers, confidence | ||
| return super()._lock_or_current(masks, centers, confidence) | ||
|
|
||
| def _menu_scores(self, frame): | ||
| if self.skip_menu_scores: | ||
| self.menu_score_frames_skipped += 1 | ||
| return 0.0, 0.0, 0.0, 0.0, 0.0, None, None | ||
| return super()._menu_scores(frame) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from ps2_autopilot.guitar_hero_vision_v11 import GuitarHeroVisionV11 | ||
|
|
||
| from .base import ProfileContext | ||
| from .guitar_hero_types import GuitarHeroPhase, GuitarHeroScreen | ||
| from .guitar_hero_v10 import GuitarHeroV10Profile | ||
|
|
||
|
|
||
| class GuitarHeroV11Profile(GuitarHeroV10Profile): | ||
| """V11: protect failure->setlist routing and cut gameplay vision latency. | ||
|
|
||
| The uploaded V9 soak showed the failure transaction itself firing Down+Confirm, | ||
| immediately followed by a false TITLE classification that reset the route to BOOT. | ||
| It then spent the rest of the run waiting/probing Start. The same bundle also | ||
| showed a critically over-budget loop, so V11 pairs lifecycle hardening with a | ||
| locked-highway fast path rather than adding another note threshold. | ||
| """ | ||
|
|
||
| policy_version = 11 | ||
|
|
||
| def __init__(self, cfg: dict) -> None: | ||
| super().__init__(cfg) | ||
| self.vision = GuitarHeroVisionV11(self.cfg) | ||
| self.failure_transition_seconds = float( | ||
| self.cfg.get("failure_transition_seconds", 5.0) | ||
| ) | ||
| self._failure_transition_until = -1e9 | ||
| self.title_guard_suppressions = 0 | ||
| self.failure_transition_wait_ticks = 0 | ||
|
|
||
| def _explicit_title_template(self, ctx: ProfileContext) -> bool: | ||
| if ctx.template is None or ctx.template.score < self.template_threshold: | ||
| return False | ||
| return self._template_screen(ctx.template.name) is GuitarHeroScreen.TITLE | ||
|
|
||
| def _classify(self, ctx: ProfileContext, obs): | ||
| # V4's image-only title splash detector intentionally has high priority during | ||
| # boot, but the live failure soak proved that it can collide with the animated | ||
| # setlist/presentation reached after NEW SONG. Once we own a non-boot route, | ||
| # only an explicit title template may reset the whole state machine to BOOT. | ||
| protected_route = self.route_stage in { | ||
| "setlist", | ||
| "difficulty", | ||
| "song", | ||
| "gameplay", | ||
| "post_song", | ||
| } | ||
| if protected_route and self.phase is not GuitarHeroPhase.BOOT and not self._explicit_title_template(ctx): | ||
| old_threshold = self.title_splash_threshold | ||
| self.title_splash_threshold = 1.01 | ||
| try: | ||
| screen = super()._classify(ctx, obs) | ||
| finally: | ||
| self.title_splash_threshold = old_threshold | ||
| if self._title_splash_score_value >= old_threshold: | ||
| self.title_guard_suppressions += 1 | ||
| return screen | ||
| return super()._classify(ctx, obs) | ||
|
|
||
| def _tick_failed_card(self, controller, ctx: ProfileContext, obs) -> str: | ||
| action = super()._tick_failed_card(controller, ctx, obs) | ||
| if action == "song failed: confirm new_song": | ||
| # Establish destination ownership immediately. Do not wait for the next | ||
| # screen classifier to infer route state from an animated transition. | ||
| self.phase = GuitarHeroPhase.MENU | ||
| self.route_stage = "setlist" | ||
| self._failure_transition_until = ctx.now + self.failure_transition_seconds | ||
| return action | ||
|
|
||
| def _tick_non_gameplay(self, controller, ctx: ProfileContext, obs) -> str: | ||
| if ctx.now < self._failure_transition_until and not self._failed_card_active: | ||
| if self.screen is GuitarHeroScreen.SETLIST: | ||
| self._failure_transition_until = -1e9 | ||
| elif self.screen in { | ||
| GuitarHeroScreen.TITLE, | ||
| GuitarHeroScreen.UNKNOWN, | ||
| GuitarHeroScreen.PRESENTATION, | ||
| }: | ||
| self._release_gameplay_inputs(controller) | ||
| self.phase = GuitarHeroPhase.MENU | ||
| self.route_stage = "setlist" | ||
| self.failure_transition_wait_ticks += 1 | ||
| return "wait for failed->setlist transition" | ||
| return super()._tick_non_gameplay(controller, ctx, obs) | ||
|
|
||
| def tick(self, controller, ctx: ProfileContext) -> str: | ||
| # Menu morphology is useful while routing, but it is wasted work once a song | ||
| # is launching/playing. On the first frame where gameplay disappears the | ||
| # runtime leaves PLAYING; the following frame automatically re-enables full | ||
| # menu scoring for results/failure reacquisition. | ||
| self.vision.skip_menu_scores = self.phase in { | ||
| GuitarHeroPhase.AWAIT_GAMEPLAY, | ||
| GuitarHeroPhase.PLAYING, | ||
| } | ||
| return super().tick(controller, ctx) | ||
|
|
||
| def telemetry(self, ctx: ProfileContext) -> dict: | ||
| state = super().telemetry(ctx) | ||
| vision = self.vision | ||
| state.update( | ||
| { | ||
| "gh_policy_version": self.policy_version, | ||
| "gh_failure_transition_active": ctx.now < self._failure_transition_until, | ||
| "gh_failure_transition_wait_ticks": self.failure_transition_wait_ticks, | ||
| "gh_title_guard_suppressions": self.title_guard_suppressions, | ||
| "gh_fast_layout_frames": vision.fast_layout_frames, | ||
| "gh_full_layout_frames": vision.full_layout_frames, | ||
| "gh_menu_score_frames_skipped": vision.menu_score_frames_skipped, | ||
| } | ||
| ) | ||
| return state | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a protected route is in
PRESENTATION—for example, if the failed-to-setlist animation outlasts the five-second guard—the inherited V2 classifier can still returnTITLEfromobs.title_score. Raising onlytitle_splash_thresholddisables V4's detector but not this second image-only path, after which V4 resets the route to BOOT and recreates the unattended-stream failure this change is intended to fix. Reject a returned image-onlyTITLEhere unless_explicit_title_template(ctx)is true.Useful? React with 👍 / 👎.