feat: Update CoreML device support and server controls
- Replace 'mps' with 'coreml' as the identifier for Apple CoreML Execution Provider across the codebase. - Remove Apple chip generation checks and ensure CoreML is available on all macOS arm64 devices. - Adjust ONNX Runtime session creation to use default CoreML configurations without forcing 'MLProgram'. - Implement support for MoGe-2 to utilize CoreML with static input shape requirements. - Enhance server panel to display restart and stop controls consistently across running states, including 'BUSY'. - Update UI tests to verify new behavior for server controls and cached model display. - Bump blendjob dependency version to 0.1.16 in packaging tests and configurations.
This commit is contained in:
@@ -17,10 +17,20 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
def setUpClass(cls):
|
||||
cls.remove_modules()
|
||||
|
||||
def draw_status_bar(_owner, _context):
|
||||
pass
|
||||
|
||||
draw_status_bar._draw_funcs = [draw_status_bar]
|
||||
|
||||
def append_status_bar(draw):
|
||||
draw._owner = "anyimage"
|
||||
draw_status_bar._draw_funcs.append(draw)
|
||||
cls.status_bar_draws.append(draw)
|
||||
|
||||
def remove_status_bar(draw):
|
||||
draw_status_bar._draw_funcs.remove(draw)
|
||||
cls.status_bar_draws.remove(draw)
|
||||
|
||||
fake_bpy = ModuleType("bpy")
|
||||
fake_bpy.props = SimpleNamespace(
|
||||
BoolProperty=lambda **options: options,
|
||||
@@ -41,8 +51,9 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
Image=type("Image", (), {}),
|
||||
Scene=type("Scene", (), {}),
|
||||
STATUSBAR_HT_header=SimpleNamespace(
|
||||
draw=draw_status_bar,
|
||||
append=append_status_bar,
|
||||
remove=lambda draw: cls.status_bar_draws.remove(draw),
|
||||
remove=remove_status_bar,
|
||||
),
|
||||
VIEW3D_MT_object_context_menu=SimpleNamespace(
|
||||
append=lambda draw: cls.context_menu_draws.append(draw),
|
||||
@@ -158,26 +169,55 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
if hasattr(self.fake_bpy.types.Image, "anyimage_upscale_source"):
|
||||
del self.fake_bpy.types.Image.anyimage_upscale_source
|
||||
|
||||
def test_mps_requires_m4_or_newer(self):
|
||||
def test_all_apple_silicon_supports_coreml(self):
|
||||
properties = self.anyimage.properties
|
||||
parse = properties.parse_apple_silicon_generation
|
||||
self.assertEqual(parse("Apple M2"), 2)
|
||||
self.assertEqual(parse("Apple M4 Pro"), 4)
|
||||
self.assertEqual(parse("Apple M10 Max"), 10)
|
||||
self.assertIsNone(parse("Intel(R) Core(TM) i9"))
|
||||
self.assertFalse(properties.supports_mps(3))
|
||||
self.assertTrue(properties.supports_mps(4))
|
||||
for generation in range(1, 5):
|
||||
with (
|
||||
self.subTest(generation=generation),
|
||||
patch.object(
|
||||
self.anyimage.runtime,
|
||||
"environment_manifest",
|
||||
return_value={},
|
||||
),
|
||||
patch.object(properties.sys, "platform", "darwin"),
|
||||
patch.object(
|
||||
properties.platform,
|
||||
"machine",
|
||||
return_value="arm64",
|
||||
),
|
||||
):
|
||||
self.assertEqual(
|
||||
properties.supported_devices(),
|
||||
["cpu", "coreml"],
|
||||
)
|
||||
|
||||
def test_unsupported_mac_hides_manifest_mps_device(self):
|
||||
def test_non_apple_silicon_mac_uses_cpu(self):
|
||||
properties = self.anyimage.properties
|
||||
manifest = {"devices": ["mps", "cpu"]}
|
||||
with (
|
||||
patch.object(
|
||||
self.anyimage.runtime,
|
||||
"environment_manifest",
|
||||
return_value={},
|
||||
),
|
||||
patch.object(properties.sys, "platform", "darwin"),
|
||||
patch.object(
|
||||
properties.platform,
|
||||
"machine",
|
||||
return_value="x86_64",
|
||||
),
|
||||
):
|
||||
self.assertEqual(properties.supported_devices(), ["cpu"])
|
||||
|
||||
def test_manifest_coreml_requires_apple_silicon(self):
|
||||
properties = self.anyimage.properties
|
||||
manifest = {"devices": ["coreml", "cpu"]}
|
||||
with (
|
||||
patch.object(
|
||||
self.anyimage.runtime,
|
||||
"environment_manifest",
|
||||
return_value=manifest,
|
||||
),
|
||||
patch.object(properties, "supports_mps", return_value=False),
|
||||
patch.object(properties.sys, "platform", "linux"),
|
||||
):
|
||||
self.assertEqual(properties.supported_devices(), ["cpu"])
|
||||
with (
|
||||
@@ -186,9 +226,34 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
"environment_manifest",
|
||||
return_value=manifest,
|
||||
),
|
||||
patch.object(properties, "supports_mps", return_value=True),
|
||||
patch.object(properties.sys, "platform", "darwin"),
|
||||
patch.object(
|
||||
properties.platform,
|
||||
"machine",
|
||||
return_value="aarch64",
|
||||
),
|
||||
):
|
||||
self.assertEqual(properties.supported_devices(), ["mps", "cpu"])
|
||||
self.assertEqual(
|
||||
properties.supported_devices(),
|
||||
["cpu", "coreml"],
|
||||
)
|
||||
|
||||
def test_manifest_rejects_old_mps_device(self):
|
||||
properties = self.anyimage.properties
|
||||
with (
|
||||
patch.object(
|
||||
self.anyimage.runtime,
|
||||
"environment_manifest",
|
||||
return_value={"devices": ["mps", "cpu"]},
|
||||
),
|
||||
patch.object(properties.sys, "platform", "darwin"),
|
||||
patch.object(
|
||||
properties.platform,
|
||||
"machine",
|
||||
return_value="arm64",
|
||||
),
|
||||
):
|
||||
self.assertEqual(properties.supported_devices(), ["cpu"])
|
||||
|
||||
def test_windows_keeps_default_directml_device(self):
|
||||
properties = self.anyimage.properties
|
||||
@@ -2957,6 +3022,105 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
offenders.append(path.relative_to(source_root))
|
||||
self.assertEqual(offenders, [])
|
||||
|
||||
def _draw_server_panel(self, state, cached_names=()):
|
||||
events = SimpleNamespace(labels=[], operators=[])
|
||||
|
||||
class Layout:
|
||||
def __init__(self, kind="layout"):
|
||||
self.kind = kind
|
||||
self.enabled = True
|
||||
|
||||
def row(self, **_options):
|
||||
return Layout("row")
|
||||
|
||||
def column(self, **_options):
|
||||
return Layout("column")
|
||||
|
||||
def label(self, **options):
|
||||
events.labels.append(
|
||||
(self.kind, options.get("text"), options.get("icon"))
|
||||
)
|
||||
|
||||
def operator(self, identifier, **_options):
|
||||
events.operators.append((identifier, self.enabled))
|
||||
return SimpleNamespace()
|
||||
|
||||
panel_module = importlib.import_module("anyimage.panel")
|
||||
panel = panel_module.ServerPanel()
|
||||
panel.layout = Layout()
|
||||
loaded = {
|
||||
str(index): name
|
||||
for index, name in enumerate(cached_names)
|
||||
}
|
||||
with (
|
||||
patch.object(
|
||||
panel_module.runtime,
|
||||
"environment_ready",
|
||||
return_value=True,
|
||||
),
|
||||
patch.object(
|
||||
panel_module.runtime,
|
||||
"server_busy",
|
||||
return_value=state == "BUSY",
|
||||
),
|
||||
patch.object(
|
||||
panel_module.runtime,
|
||||
"server_status",
|
||||
return_value={
|
||||
"state": state,
|
||||
"message": state.title(),
|
||||
"resources": {
|
||||
"model_manager": {"loaded": loaded},
|
||||
},
|
||||
},
|
||||
),
|
||||
):
|
||||
panel.draw(None)
|
||||
return events
|
||||
|
||||
def test_server_panel_shows_controls_for_every_running_state(self):
|
||||
for state, label, icon in (
|
||||
("READY", "Running", "RADIOBUT_ON"),
|
||||
("BUSY", "Running (Busy)", "TIME"),
|
||||
):
|
||||
with self.subTest(state=state):
|
||||
events = self._draw_server_panel(state)
|
||||
operators = [identifier for identifier, _enabled in events.operators]
|
||||
self.assertIn(("row", label, icon), events.labels)
|
||||
self.assertIn("anyimage.restart_server", operators)
|
||||
self.assertIn("anyimage.stop_server", operators)
|
||||
|
||||
for state in ("STOPPED", "STARTING", "ERROR"):
|
||||
with self.subTest(state=state):
|
||||
events = self._draw_server_panel(state)
|
||||
operators = [identifier for identifier, _enabled in events.operators]
|
||||
self.assertNotIn("anyimage.restart_server", operators)
|
||||
self.assertNotIn("anyimage.stop_server", operators)
|
||||
|
||||
def test_server_panel_wraps_multiple_cached_models(self):
|
||||
for cached_names, expected_labels in (
|
||||
((), [("row", "Cached: None")]),
|
||||
(("Model A",), [("row", "Cached: Model A")]),
|
||||
(
|
||||
("Model A", "Model B"),
|
||||
[
|
||||
("column", "Cached:"),
|
||||
("column", "Model A"),
|
||||
("column", "Model B"),
|
||||
],
|
||||
),
|
||||
):
|
||||
with self.subTest(cached_names=cached_names):
|
||||
events = self._draw_server_panel("READY", cached_names)
|
||||
cached_labels = [
|
||||
(kind, text)
|
||||
for kind, text, _icon in events.labels
|
||||
if text != "Running"
|
||||
]
|
||||
self.assertEqual(cached_labels, expected_labels)
|
||||
operators = [identifier for identifier, _enabled in events.operators]
|
||||
self.assertIn("anyimage.clear_models", operators)
|
||||
|
||||
def test_status_bar_reads_runtime_state(self):
|
||||
calls = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user