diff --git a/Documentation/devicetree/bindings/display/apple,dcp.yaml b/Documentation/devicetree/bindings/display/apple,dcp.yaml index a7c74d948e6e9c..09df19ef1b3e6b 100644 --- a/Documentation/devicetree/bindings/display/apple,dcp.yaml +++ b/Documentation/devicetree/bindings/display/apple,dcp.yaml @@ -23,6 +23,7 @@ properties: - apple,t8112-dcp - apple,t6000-dcp - apple,t6020-dcp + - apple,t8122-dcp - apple,t8140-dcp - apple,t8142-dcp - const: apple,dcp @@ -32,6 +33,7 @@ properties: - apple,t8112-dcpext - apple,t6000-dcpext - apple,t6020-dcpext + - apple,t8122-dcpext - const: apple,dcpext - items: - const: apple,t8140-dcpext @@ -180,6 +182,38 @@ properties: maxItems: 5 description: Loader-provided firmware ABI version used for IOMFB selection. + apple,j613-25g83-dcpext-handoff: + $ref: /schemas/types.yaml#/definitions/uint32 + const: 1 + description: | + Loader validation of the J613 native26 external processor. Requires + the separately validated internal mapping handoff, exact 26.6.2 image, + complete stream-5 mappings and retained locked page tables. Does not + enable hardware. Firmware reservations must also be complete. + + apple,j613-25g83-dcpext-boot-state: + $ref: /schemas/types.yaml#/definitions/uint32-array + minItems: 4 + maxItems: 4 + description: | + Loader observations in order of powered ancestry, ASC CPU control, + ASC CPU status and firmware-DART protection. Read only while the + processor's full power ancestry is active. A powered processor must + have protected inherited roots; this is not an RTKit readiness claim. + + apple,t8122-dcpext-firmware-ready: + $ref: /schemas/types.yaml#/definitions/uint32 + const: 1 + description: All external firmware reservations were committed by the loader. + + apple,firmware-uuid: + $ref: /schemas/types.yaml#/definitions/string + description: UUID of the actual loaded display image, read from its iop nub. + + apple,pmp: + $ref: /schemas/types.yaml#/definitions/phandle + description: Native PMP supplier, independent of the report request entry. + apple,connector-type: enum: [HDMI-A, DP, USB-C] diff --git a/MAINTAINERS b/MAINTAINERS index 491823243a5fc1..d152848f6be827 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2723,7 +2723,9 @@ F: drivers/pwm/pwm-apple.c F: drivers/reset/reset-apple-cio.c F: drivers/rtc/rtc-macsmc.c F: drivers/soc/apple/* +F: tools/asahi/m3-uapi-* F: tools/asahi/pmp-bootargs-tests.rs +F: tools/asahi/test-m3-uapi.py F: drivers/spi/spi-apple.c F: drivers/spmi/spmi-apple-controller.c F: drivers/thunderbolt/.kunitconfig.apple-dpin diff --git a/arch/arm64/boot/dts/apple/t8122-j613-dcp-25g83.dtsi b/arch/arm64/boot/dts/apple/t8122-j613-dcp-25g83.dtsi index e292dda82f0227..57bb0b13bf614a 100644 --- a/arch/arm64/boot/dts/apple/t8122-j613-dcp-25g83.dtsi +++ b/arch/arm64/boot/dts/apple/t8122-j613-dcp-25g83.dtsi @@ -20,7 +20,8 @@ }; &dcpext { - /delete-property/ apple,pmp-report; + apple,pmp-report = <&pmp_report_dispext>; + apple,pmp = <&pmp>; }; &disp0_piodma { @@ -37,10 +38,17 @@ /* This profile requests only DISP and ANS until external admission succeeds. */ &pmp_report_dispext { + apple,soc-device-record; status = "disabled"; }; +&pmp_report_disp { + apple,soc-device-record; +}; + &pmp_report_ans { + apple,soc-device-record; + apple,no-ack; apple,always-on; }; @@ -89,3 +97,12 @@ apple,pmp = <&pmp>; status = "disabled"; }; + +/* Native26 firmware and PIODMA use the same 1-TiB alias as the panel. */ +&dcpext_dart { + apple,dma-range = /bits/ 64 <0x10000000000 0x1000000000>; +}; + +&dispext0_dart { + apple,dma-range = /bits/ 64 <0x10000000000 0x1000000000>; +}; diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index de9c3846b15b8e..8c07e9f4fafa1e 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -65,7 +65,8 @@ void afk_service_retry(struct apple_dcp_afkep *ep) { unsigned long flags; - if (!ep || !ep->wire_v2 || ep != ep->dcp->dcpavservep) + if (!ep || !ep->wire_v2 || + (!dcp_is_ext26(ep->dcp) && ep != ep->dcp->dcpavservep)) return; spin_lock_irqsave(&ep->lock, flags); if (!ep->stopping) @@ -103,6 +104,7 @@ struct apple_dcp_afkep *afk_init(struct apple_dcp *dcp, u32 endpoint, afkep->ops = ops; afkep->dcp = dcp; afkep->endpoint = endpoint; + afkep->wire_v2 = dcp_is_ext26(dcp); afkep->wq = alloc_ordered_workqueue("apple-dcp-afkep%02x", WQ_MEM_RECLAIM, endpoint); if (!afkep->wq) { @@ -424,8 +426,12 @@ static void afk_retry_service_init(struct apple_dcp_afkep *ep) if (!service->pending_open) continue; - ret = ops->revalidate(service, service->pending_name, - service->pending_class, service->pending_unit); + if (dcp_is_ext26(ep->dcp) && + (!READ_ONCE(ep->dcp->ext26_services_open) || + (ep == ep->dcp->dptxep && !dptx_native_controllers_ready(ep->dcp)))) + continue; + ret = ops->revalidate ? ops->revalidate(service, service->pending_name, + service->pending_class, service->pending_unit) : 0; if (ret) { afk_discard_pending_open(service); continue; @@ -443,8 +449,9 @@ static void afk_retry_service_init(struct apple_dcp_afkep *ep) } service->pending_open = false; afk_service_publish(service); - ops->init(service, service->pending_name, service->pending_class, - service->pending_unit); + if (ops->init) + ops->init(service, service->pending_name, service->pending_class, + service->pending_unit); if (service->enabled) afk_populate_service_debugfs(service); kfree(service->pending_name); @@ -477,8 +484,8 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, const char *epic_name = NULL, *epic_class = NULL; const struct apple_epic_service_ops *ops; struct dcp_parse_ctx ctx; - u8 *props = payload + sizeof(name); - size_t props_size = payload_size - sizeof(name); + u8 *props; + size_t props_size; previous = afk_epic_find_service(ep, channel); /* @@ -503,7 +510,8 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, if (!pending->pending_open || pending->channel != channel) continue; - if (!pending->ops->revalidate(pending, pending->pending_name, + if (!pending->ops->revalidate || + !pending->ops->revalidate(pending, pending->pending_name, pending->pending_class, pending->pending_unit)) return; afk_discard_pending_open(pending); @@ -516,6 +524,8 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, } strscpy(name, payload, sizeof(name)); + props = payload + sizeof(name); + props_size = payload_size - sizeof(name); /* * in DCP firmware 13.2 DCP reports interface-name as name which starts @@ -532,8 +542,18 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, return; } ret = parse_epic_service_init(&ctx, &epic_name, &epic_class, &epic_unit, - ep->dcp->fw_compat == DCP_FIRMWARE_H17P); + ep->dcp->fw_compat == DCP_FIRMWARE_H17P && + !dcp_is_ext26(ep->dcp)); if (ret) { + /* These native infrastructure reports have no EPIC identity. */ + if (dcp_is_ext26(ep->dcp) && ret == -ENOENT && + (ep->endpoint == SYSTEM_ENDPOINT || + ep->endpoint == DISP0_ENDPOINT)) { + dev_dbg(ep->dcp->dev, + "native26 AFK ep=%#x interface=%u report=%s has no EPIC identity; ignored\n", + ep->endpoint, channel, name); + return; + } dev_err(ep->dcp->dev, "AFK[ep:%02x]: failed to extract init props: %d\n", ep->endpoint, ret); @@ -548,6 +568,10 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, ops = afk_match_service(ep, epic_name); else ops = afk_match_service(ep, service_name); + if (dcp_is_ext26(ep->dcp)) + dev_dbg(ep->dcp->dev, + "native26 AFK identity ep=%#x interface=%u name=%s class=%s unit=%lld\n", + ep->endpoint, channel, epic_name ?: name, epic_class ?: "", epic_unit); /* T6030 14.7: the DPAV controller endpoint binds whatever it announces. */ if (!ops && ep->dcp->fw_compat == DCP_FIRMWARE_V_14_7 && ep->ops && @@ -558,6 +582,9 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, ops = ep->ops; } + if (!ops && dcp_is_ext26(ep->dcp) && ep->ops && + !strcmp(ep->ops->name, "native26-observer")) + ops = ep->ops; if (!ops) { dev_err(ep->dcp->dev, "AFK[ep:%02x]: unable to match service %s on channel %d\n", @@ -588,13 +615,19 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, "AFK optional %s (%s) is unsupported on channel %u\n", ops->name, service_name, channel); else - dev_err(ep->dcp->dev, "AFK service %s admission failed: %d\n", - service_name, ret); + dev_err(ep->dcp->dev, + "AFK service %s admission failed: %d (ep=%#x interface=%u name=%s unit=%lld)\n", + service_name, ret, ep->endpoint, channel, epic_name ?: name, epic_unit); afk_service_discard(service); goto free; } } - if (previous && retired && ops->revalidate) { + if ((previous && retired && ops->revalidate) || + (dcp_is_ext26(ep->dcp) && + (!READ_ONCE(ep->dcp->ext26_services_open) || + (ep == ep->dcp->dptxep && !dptx_native_controllers_ready(ep->dcp))))) { + /* No reader may use a native interface before its deferred OPEN. */ + WRITE_ONCE(service->enabled, false); /* Preserve the admitted identity and its original provider ticket. */ service->pending_open = true; service->pending_name = epic_name; @@ -1828,6 +1861,22 @@ static int afk_send_command_common(struct apple_epic_service *service, u8 type, goto err_put_service; } + if (dcp_is_ext26(ep->dcp)) { + size_t charge = ALIGN(payload_len, SZ_16K) + ALIGN(output_len, SZ_16K); + + spin_lock_irqsave(&ep->lock, flags); + ret = READ_ONCE(ep->dcp->crashed) ? -EIO : + (ep->ext26_command_count >= 255 || + charge > SZ_16M - ep->ext26_command_bytes) ? -ENOSPC : 0; + if (!ret) { + ep->ext26_command_bytes += charge; + ep->ext26_command_count++; + } + spin_unlock_irqrestore(&ep->lock, flags); + if (ret) + goto err_put_service; + } + rxbuf = dma_alloc_coherent(ep->dcp->dev, output_len, &rxbuf_dma, GFP_KERNEL); if (!rxbuf) { @@ -1921,7 +1970,9 @@ static int afk_send_command_common(struct apple_epic_service *service, u8 type, */ if (!service->cmds[idx].done) { service->cmds[idx].completion = NULL; - service->cmds[idx].free_on_ack = true; + service->cmds[idx].free_on_ack = !dcp_is_ext26(ep->dcp); + if (dcp_is_ext26(ep->dcp)) + WRITE_ONCE(ep->dcp->crashed, true); spin_unlock_irqrestore(&service->lock, flags); dev_warn(ep->dcp->dev, "AFK[ep:%02x]: %s (chan:%u) command type 0x%x tag 0x%04x timed out after %u ms\n", @@ -1985,9 +2036,11 @@ static int afk_send_command_common(struct apple_epic_service *service, u8 type, spin_unlock_irqrestore(&service->lock, flags); err_free_txbuf: kfree(inline_command); - dma_free_coherent(ep->dcp->dev, payload_len, txbuf, txbuf_dma); + if (!dcp_is_ext26(ep->dcp)) + dma_free_coherent(ep->dcp->dev, payload_len, txbuf, txbuf_dma); err_free_rxbuf: - dma_free_coherent(ep->dcp->dev, output_len, rxbuf, rxbuf_dma); + if (!dcp_is_ext26(ep->dcp)) + dma_free_coherent(ep->dcp->dev, output_len, rxbuf, rxbuf_dma); err_put_service: afk_service_put(service); return ret; @@ -2346,3 +2399,26 @@ static void afk_remove_service_debugfs(struct apple_epic_service *srv) #if IS_ENABLED(CONFIG_DRM_APPLE_PARSER_KUNIT_TEST) #include "afk-parser-test.c" #endif + +/* Called after RX and the ordered endpoint worker have been joined. */ +bool afk_host_idle(struct apple_dcp_afkep *ep) +{ + unsigned long flags; + unsigned int i; + bool idle = true; + + spin_lock_irqsave(&ep->lock, flags); + if (ep->rxbfr.ready) + idle = READ_ONCE(*ep->rxbfr.rptr) == READ_ONCE(*ep->rxbfr.wptr); + if (ep->txbfr.ready) + idle &= READ_ONCE(*ep->txbfr.rptr) == READ_ONCE(*ep->txbfr.wptr); + spin_unlock_irqrestore(&ep->lock, flags); + for (i = 0; i < AFK_MAX_CHANNEL && idle; i++) { + struct apple_epic_service *service = &ep->services[i]; + + spin_lock_irqsave(&service->lock, flags); + idle = bitmap_empty(service->cmd_map, MAX_PENDING_CMDS); + spin_unlock_irqrestore(&service->lock, flags); + } + return idle; +} diff --git a/drivers/gpu/drm/apple/afk.h b/drivers/gpu/drm/apple/afk.h index 2b6609abe361a5..adc290caaae871 100644 --- a/drivers/gpu/drm/apple/afk.h +++ b/drivers/gpu/drm/apple/afk.h @@ -449,6 +449,8 @@ struct apple_dcp_afkep { void *bfr; u16 bfr_tag; size_t bfr_size; + size_t ext26_command_bytes; + unsigned int ext26_command_count; dma_addr_t bfr_dma; struct afk_ringbuffer txbfr; @@ -474,6 +476,8 @@ struct apple_dcp_afkep *afk_init(struct apple_dcp *dcp, u32 endpoint, int afk_v2_copy_owned(struct apple_epic_service *service, u64 dva, void *buffer, size_t size, bool to_peer); int afk_start(struct apple_dcp_afkep *ep); +/* A sampled host boundary, never a remote-producer or DMA fence. */ +bool afk_host_idle(struct apple_dcp_afkep *ep); void afk_shutdown(struct apple_dcp_afkep *ep); void afk_quiesce(struct apple_dcp_afkep *ep); int afk_receive_message(struct apple_dcp_afkep *ep, u64 message); diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index 2324a8acd4c72a..86c9380a473bc7 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -290,8 +290,13 @@ static void apple_crtc_atomic_enable(struct drm_crtc *crtc, /* A cable generation can change while the CRTC stays active. */ dcp_external_recover(to_apple_crtc(crtc)->dcp); } - if (crtc_state->active) - dcp_crtc_atomic_modeset(crtc, commit); + if (crtc_state->active) { + ret = dcp_crtc_atomic_modeset(crtc, commit); + if (ret && dcp_is_ext26(dcp)) { + dev_err(dcp->dev, "native26 modeset failed (%d); releasing pending flip\n", ret); + goto failed; + } + } dcp_typec_follow_done(locked); return; @@ -683,7 +688,7 @@ static int apple_probe_per_dcp(struct device *dev, planes[zpos] = apple_plane_init(drm, 1U << num, surf, supports_l10r, supports_xrgb2101010, - plane_type, interchange); + plane_type, interchange, dcp_is_ext26(dcp_data)); if (IS_ERR(planes[zpos])) return PTR_ERR(planes[zpos]); diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 29321f8032a490..4b9a080c164542 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -426,7 +426,7 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) int ret = 0; guard(mutex)(&dcp->external_lock); - if (dcp_is_external_h17p(dcp) && + if (dcp_is_retained_external(dcp) && (dcp->external_error || (dcp->external_power_attempted && !dcp->external_stopped))) return dcp->external_error ?: -EAGAIN; @@ -478,7 +478,7 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) if (sel && sel != -EOPNOTSUPP) dev_warn(dcp->dev, "DP tunnel source reset failed: %d\n", sel); } - if (dcp_is_external_h17p(dcp)) + if (dcp_is_retained_external(dcp)) dpavservep_detach(dcp); route->selected = false; ended_attachment = dcp_modes_end_typec(dcp, route); @@ -1165,7 +1165,7 @@ static bool dcp_rebalance_deactivate(void *data, void *entry) (dcp->typec_connector && dcp->typec_connector->connected)) { ret = dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0); /* H17P external pipes keep their own stop recovery, as before. */ - if (ret && !dcp_is_external_h17p(dcp)) + if (ret && !dcp_is_retained_external(dcp)) return true; } port->hpd = false; @@ -1297,7 +1297,7 @@ static int dcp_typec_route_prepare(struct typec_mux_dev *mux, port->dp_wanted = false; port->dp_hpd = false; /* HPD absence alone is not proof of a completed firmware shutdown. */ - if (dcp_is_external_h17p(dcp)) { + if (dcp_is_retained_external(dcp)) { scoped_guard(mutex, &dcp->external_lock) { if (dcp->external_error) return dcp->external_error; @@ -1435,7 +1435,7 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, (dcp->typec_connector && dcp->typec_connector->connected)) { ret = dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0); /* H17P external pipes keep their own stop recovery, as before. */ - if (ret && !dcp_is_external_h17p(dcp)) + if (ret && !dcp_is_retained_external(dcp)) return ret; } } @@ -1763,7 +1763,7 @@ int dcp_direct_crossbar_link(struct apple_dcp *dcp, bool up) struct mux_control *xbar = NULL; int ret; - if (!of_machine_is_compatible("apple,t6030")) + if (!of_machine_is_compatible("apple,t6030") && !dcp_is_ext26(dcp)) return 0; guard(mutex)(&dcp->tb_lock); route = dcp->active_typec_route; @@ -1774,7 +1774,7 @@ int dcp_direct_crossbar_link(struct apple_dcp *dcp, bool up) /* A deselected output went idle, and its clocks with it. */ if (dcp->direct_xbar_up && dcp->direct_xbar_up != xbar) dcp->direct_xbar_up = NULL; - if (!xbar || !!dcp->direct_xbar_up == up) + if (!xbar || (!!dcp->direct_xbar_up == up && (up || !dcp_is_ext26(dcp)))) return 0; ret = dcp_dpxbar_link(xbar, up); if (ret) { @@ -3368,7 +3368,7 @@ static void dcp_typec_route_unregister(void *data) if (port->hpd || dcp->typec_cable_connected) dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0); port->hpd = false; - if (dcp_typec_route_deactivate(route) && dcp_is_external_h17p(dcp)) + if (dcp_typec_route_deactivate(route) && dcp_is_retained_external(dcp)) return; port->owner = NULL; } @@ -3379,7 +3379,7 @@ static void dcp_typec_route_unregister(void *data) dcp_tb_binding_retire(dcp, route); if (port->hpd || dcp->typec_cable_connected) dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0); - if (dcp_typec_route_deactivate(route) && dcp_is_external_h17p(dcp)) + if (dcp_typec_route_deactivate(route) && dcp_is_retained_external(dcp)) return; port->secondary_owner = NULL; } diff --git a/drivers/gpu/drm/apple/dcp-fabric.h b/drivers/gpu/drm/apple/dcp-fabric.h index 1176ef56874f35..5bd1dd8d0b41cd 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.h +++ b/drivers/gpu/drm/apple/dcp-fabric.h @@ -118,5 +118,6 @@ void dcp_fabric_shutdown_dptx(struct apple_dcp *dcp); int dcp_dptx_connect(struct apple_dcp *dcp, u32 port); int dcp_dptx_disconnect(struct apple_dcp *dcp, u32 port); bool dcp_is_external_h17p(struct apple_dcp *dcp); +bool dcp_is_retained_external(struct apple_dcp *dcp); #endif /* __APPLE_DCP_FABRIC_H__ */ diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 3eac22a57aa069..b2349b871c7f1b 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -39,6 +39,7 @@ #define DCP_MAX_PLANES 4 +struct ext26_control; struct apple_dcp_afkep; struct dcp_method_entry; @@ -183,6 +184,7 @@ enum dcp_iomfb_method_profile { DCP_IOMFB_METHODS_DEFAULT, DCP_IOMFB_METHODS_H17G, DCP_IOMFB_METHODS_H17P_EXTERNAL, + DCP_IOMFB_METHODS_J613_EXT26, }; struct apple_dcp_hw_data { @@ -322,6 +324,41 @@ struct apple_dcp { /* DCP has crashed */ bool crashed; + /* Native26 control/property epochs, serialized by the IOMFB owner. */ + struct completion ext26_link_ready; + bool ext26_services_open; + bool ext26_client_open; + struct apple_dcp_afkep *ext26_aux[3]; + u32 ext26_stride; + u64 ext26_edid_generation; + bool ext26_edid_tried; + u64 ext26_rx_ns; + u64 ext26_power_ns; + u64 ext26_generation; + u64 ext26_buffer_bytes; + bool ext26_blocked; + bool ext26_pm_work_off; + bool ext26_stopping; + bool ext26_blanked; + bool ext26_pm_preparing; + bool ext26_control_pending; + bool ext26_early_complete; + u32 ext26_early_id; + u64 ext26_early_ns; + u32 ext26_control_id; + struct ext26_control *ext26_control; + struct work_struct ext26_blank_work; + struct list_head ext26_retained; + u64 ext26_retained_bytes; + unsigned int ext26_epochs; + struct drm_display_mode ext26_mode; + DECLARE_BITMAP(ext26_released, DCP_MAX_MAPPINGS); + bool ext26_power_sent; + bool ext26_normal; + struct completion ext26_ready; + iomfb_cb_handler *ext26_handlers; + u8 ext26_present[0x1bd8]; + DECLARE_BITMAP(iomfb_surfaces, DCP_MAX_PLANES); @@ -734,6 +771,11 @@ static inline bool dcp_fw_is_14_7(const struct apple_dcp *dcp) dcp->hw.fw_14_7_endpoints); } +static inline bool dcp_is_ext26(const struct apple_dcp *dcp) +{ + return dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_J613_EXT26; +} + /* * The T6030 external processor (the J514S/J516S built-in HDMI port and * USB-C displays) driven natively through Aurora's DCPLink session: DCP @@ -766,8 +808,8 @@ void dcp_external_audio_start(struct apple_dcp *dcp); static inline bool dcp_ext_dptx_607(const struct apple_dcp *dcp) { /* Not the generic M1/M2 DCPs this tree also runs on the template. */ - return dcp->fw_compat == DCP_FIRMWARE_V_14_7_TEMPLATE && - dcp->hw.fw_14_7_endpoints; + return (dcp->fw_compat == DCP_FIRMWARE_V_14_7_TEMPLATE && + dcp->hw.fw_14_7_endpoints); } #define DCP_AUDIO_MAX_CHANS 15 diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index a8571b5446f5c4..06a5908828898a 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -2,6 +2,7 @@ /* Copyright 2021 Alyssa Rosenzweig */ #include +#include #include #include #include @@ -51,9 +52,11 @@ #include "dcp-hdmi.h" #include "dcpext_scanout.h" #include "dcp-internal.h" +#include "j613-25g83.h" #include "dcp-external-bw.h" #include "dptx-service.h" #include "iomfb.h" +#include "iomfb_internal.h" #include "ibootep.h" #include "parser.h" #include "trace.h" @@ -215,7 +218,49 @@ bool dcp_is_external_h17p(struct apple_dcp *dcp) return dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL; } +bool dcp_is_retained_external(struct apple_dcp *dcp) +{ + return dcp_is_external_h17p(dcp) || dcp_is_ext26(dcp); +} + +static void dcp_ext26_blank_work(struct work_struct *work) +{ + struct apple_dcp *dcp = container_of(work, struct apple_dcp, ext26_blank_work); + int ret; + + guard(mutex)(&dcp->external_lock); + if (dcp->ext26_pm_preparing || dcp->external_stopped || + !READ_ONCE(dcp->pipe_enabled_h17p)) + return; + iomfb_pause_presentation(dcp, true); + ret = iomfb_ext26_blank(dcp); + if (ret) { + WRITE_ONCE(dcp->ext26_blocked, true); + dcp->external_error = ret; + dev_err(dcp->dev, "native26 null control failed: %d; uncertain backing retained\n", ret); + } else { + WRITE_ONCE(dcp->ext26_blanked, true); + dev_dbg(dcp->dev, "native26 DPMS blank complete; link, mode and backing retained\n"); + } +} + static int dcp_external_stop_locked(struct apple_dcp *dcp, u64 generation); +int dcp_ext26_layout_stop(struct apple_dcp *dcp) +{ + int ret; + + /* Atomic modeset owns external_lock; poweron joined the blank worker. */ + lockdep_assert_held(&dcp->external_lock); + ret = dcp_external_stop_locked(dcp, dcp->external_generation); + if (!ret) { + WRITE_ONCE(dcp->ext26_blanked, false); + dcp->external_generation++; + dcp_queue_typec_reconnect(dcp, 0); + return -EAGAIN; /* A fresh owner/catalogue/backing is required. */ + } + return ret; +} + static int dcp_external_disconnect(struct apple_dcp *dcp, bool physical); /* copied and simplified from drm_vblank.c */ @@ -634,6 +679,14 @@ static void dcp_delayed_vblank(struct work_struct *work) static struct apple_dcp_afkep *dcp_afkep(struct apple_dcp *dcp, u8 endpoint) { + if (dcp_is_ext26(dcp)) { + if (endpoint == 0x27) + return dcp->ext26_aux[0]; + if (endpoint == 0x29) + return dcp->ext26_aux[1]; + if (endpoint == 0x2c) + return dcp->ext26_aux[2]; + } /* DPAV 0x2a on H17P overlaps the legacy DPTX endpoint number. */ if (dcp->dcpavservep && endpoint == dcp->dcpavservep->endpoint) return dcp->dcpavservep; @@ -710,7 +763,7 @@ void dcp_swap_watchdog_complete(struct apple_dcp *dcp) void dcp_external_link_fault(struct apple_dcp *dcp) { dcp_180hz_invalidate(dcp); - if (dcp_is_external_h17p(dcp) && dcp_mode_invalidate(&dcp->mode_state)) + if (dcp_is_retained_external(dcp) && dcp_mode_invalidate(&dcp->mode_state)) dcp_handle_hotplug_actions(dcp, DCP_HOTPLUG_NOTIFY); } @@ -755,6 +808,15 @@ static void dcp_recv_msg(void *cookie, u8 endpoint, u64 message) afk_receive_message(ep, message); } +static void dcp_recv_msg_timed(void *cookie, u8 endpoint, u64 message, u64 received_ns) +{ + struct apple_dcp *dcp = cookie; + + if (dcp_is_ext26(dcp) && endpoint == IOMFB_ENDPOINT) + dcp->ext26_rx_ns = received_ns; + dcp_recv_msg(cookie, endpoint, message); +} + static void dcp_rtk_crashed(void *cookie, const void *crashlog, size_t crashlog_size) { struct apple_dcp *dcp = cookie; @@ -878,6 +940,7 @@ static void dcp_rtk_shmem_destroy(void *cookie, struct apple_rtkit_shmem *bfr) static struct apple_rtkit_ops rtkit_ops = { .crashed = dcp_rtk_crashed, .recv_message = dcp_recv_msg, + .recv_message_timed = dcp_recv_msg_timed, .shmem_setup = dcp_rtk_shmem_setup, .shmem_destroy = dcp_rtk_shmem_destroy, }; @@ -886,6 +949,7 @@ static const struct apple_rtkit_ops external_h17p_rtkit_ops = { .flags = APPLE_RTKIT_COPROC_PLACES_BUFFERS, .crashed = dcp_rtk_crashed, .recv_message = dcp_recv_msg, + .recv_message_timed = dcp_recv_msg_timed, .shmem_setup = dcp_rtk_shmem_setup, .shmem_destroy = dcp_rtk_shmem_destroy, }; @@ -895,7 +959,8 @@ void dcp_send_message(struct apple_dcp *dcp, u8 endpoint, u64 message) bool native_14_7 = dcp->fw_compat == DCP_FIRMWARE_V_14_7; int ret; - if (READ_ONCE(dcp->quiescing)) + if (READ_ONCE(dcp->quiescing) || + (dcp_is_ext26(dcp) && READ_ONCE(dcp->ext26_blocked))) return; trace_dcp_send_msg(dcp, endpoint, message); /* @@ -904,7 +969,11 @@ void dcp_send_message(struct apple_dcp *dcp, u8 endpoint, u64 message) * DisplayPort handshake then waits for a reply that was never sent. */ ret = apple_rtkit_send_message(dcp->rtk, endpoint, message, NULL, - native_14_7 ? in_atomic() : true); + (native_14_7 || dcp_is_ext26(dcp)) ? in_atomic() : true); + if (ret && dcp_is_ext26(dcp)) { + WRITE_ONCE(dcp->crashed, true); + dev_err(dcp->dev, "native26 mailbox send failed ep=%#x: %d\n", endpoint, ret); + } if (ret && native_14_7) dev_warn_ratelimited(dcp->dev, "DCP send ep %02x failed: %d\n", endpoint, ret); @@ -1030,7 +1099,7 @@ static bool dcp_connect_session_valid(struct apple_dcp *dcp, /* H17P (J700, T8140, T8142) connects and reconnects as before, without one. */ static bool dcp_typec_sessionless(struct apple_dcp *dcp) { - return dcp->fw_compat == DCP_FIRMWARE_H17P || dcp_is_external_h17p(dcp); + return dcp->fw_compat == DCP_FIRMWARE_H17P || dcp_is_retained_external(dcp); } /* Capture the session at enqueue, not when a stale worker eventually runs. */ @@ -1122,6 +1191,11 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable dcp->dptxport[port].atcphy = dcp->phy; /* a tiled display's second half comes in on the port's dpin1 */ dfp_port = port && READ_ONCE(dcp->split.active) ? 2 : dcp->dptx_dfp_port; + if (dcp_is_ext26(dcp)) { + ret = dcp_direct_crossbar_link(dcp, false); + if (ret) + goto out_unlock; + } if (!dcp_ext_dptx_607(dcp)) { ret = dptxport_validate_connection(dcp->dptxport[port].service, dfp_port, @@ -1250,7 +1324,7 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable /* Do not timestamp a new attachment after an older link wait. */ if (!port && dcp_is_typec_output(dcp) && !dcp_is_usb4_output(dcp) && dcp->fw_compat != DCP_FIRMWARE_H17P && !dcp->external_native && - !dcp_is_external_h17p(dcp)) { + !dcp_is_retained_external(dcp)) { guard(mutex)(&dcp->hpd_mutex); if (dcp->typec_generation == generation && dcp->typec_cable_connected) { scoped_guard(spinlock_irqsave, &dcp->relink_lock) @@ -1262,7 +1336,7 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable out_withdraw: mutex_lock(&dcp->hpd_mutex); - if (!dcp_is_external_h17p(dcp)) { + if (!dcp_is_retained_external(dcp)) { dcp->dptxport[port].connected = false; goto out_release; } @@ -1332,7 +1406,9 @@ static int dcp_external_link_locked(struct apple_dcp *dcp) int ret; lockdep_assert_held(&dcp->external_lock); - if (!dcp->external_prepared) + if (!dcp->external_prepared || + (dcp_is_ext26(dcp) && (!READ_ONCE(dcp->ext26_services_open) || + !dptx_native_controllers_ready(dcp)))) return -EAGAIN; if (dcp->external_error) return dcp->external_error; @@ -1350,8 +1426,18 @@ static int dcp_external_start_locked(struct apple_dcp *dcp) lockdep_assert_held(&dcp->external_lock); ret = dcp_external_link_locked(dcp); - if (ret || dcp->external_running) + if (ret) return ret; + /* Gentle DPMS kept the link running but paused presentation. */ + if (dcp_is_ext26(dcp)) { + dcp->external_running = true; + dcp->external_stopped = false; + WRITE_ONCE(dcp->typec_crtc_off, false); + iomfb_pause_presentation(dcp, false); + return 0; + } + if (dcp->external_running) + return 0; /* A deferred stop sent nothing: the pipe is still powered. */ if (dcp->external_stop_deferred) { dcp->external_stop_deferred = false; @@ -1621,7 +1707,7 @@ int dcp_dptx_connect(struct apple_dcp *dcp, u32 port) struct dcp_fabric_session session = {}; int ret; - if (!dcp_is_external_h17p(dcp)) { + if (!dcp_is_retained_external(dcp)) { if (dcp_typec_sessionless(dcp)) { ret = dcp_dptx_connect_raw(dcp, port, NULL, NULL, false, false); } else { @@ -1655,6 +1741,10 @@ static int dcp_external_stop_locked(struct apple_dcp *dcp, u64 generation) if (dcp->external_stopped && !READ_ONCE(dcp->dptxport[0].connected)) return 0; iomfb_pause_presentation(dcp, true); + if (dcp_is_ext26(dcp)) { + WRITE_ONCE(dcp->ext26_stopping, true); + dpavservep_detach(dcp); + } dcp->external_running = false; /* Route setup can call this before IOMFB or its shared memory exists. */ if (dcp->external_stopped || !dcp->external_power_attempted) @@ -1704,7 +1794,33 @@ static int dcp_external_stop_locked(struct apple_dcp *dcp, u64 generation) dcp->dptxport[0].connected = false; } } + if (dcp_is_ext26(dcp)) { + struct apple_dcp_afkep *endpoints[] = {dcp->systemep, dcp->ibootep, + dcp->dptx_controller_ep, dcp->dptxep, dcp->dcpavservep, + dcp->ext26_aux[0], dcp->ext26_aux[1], dcp->ext26_aux[2]}; + unsigned int i; + + step = "draining the retained epoch"; + apple_rtkit_flush_rx(dcp->rtk); + for (i = 0; i < ARRAY_SIZE(endpoints); i++) + if (!IS_ERR_OR_NULL(endpoints[i])) + flush_workqueue(endpoints[i]->wq); + apple_rtkit_flush_rx(dcp->rtk); + ret = iomfb_queue_settle(dcp, msecs_to_jiffies(5000)); + for (i = 0; !ret && i < ARRAY_SIZE(endpoints); i++) + if (!IS_ERR_OR_NULL(endpoints[i]) && !afk_host_idle(endpoints[i])) + ret = -EBUSY; + if (!ret) + ret = iomfb_ext26_retain_epoch(dcp); + if (ret) + goto failed; + dcp_modes_begin_attachment(dcp); + WRITE_ONCE(dcp->pipe_enabled_h17p, false); + WRITE_ONCE(dcp->ext26_normal, false); + } dcp->external_stopped = true; + if (dcp_is_ext26(dcp)) + WRITE_ONCE(dcp->ext26_stopping, false); WRITE_ONCE(dcp->typec_crtc_off, true); if (dcp->avep) av_service_disconnect(dcp); @@ -1722,10 +1838,14 @@ static int dcp_external_disconnect(struct apple_dcp *dcp, bool physical) cancel_delayed_work_sync(&dcp->typec_reconnect_wq); cancel_delayed_work_sync(&dcp->placeholder_edid_wq); + if (dcp_is_ext26(dcp)) + flush_work(&dcp->ext26_blank_work); guard(mutex)(&dcp->external_lock); /* Invalidate physical EDID before waiting for firmware power/link teardown. */ if (physical) { WRITE_ONCE(dcp->external_physically_absent, true); + if (dcp_is_ext26(dcp)) + dcp_modes_begin_attachment(dcp); scoped_guard(mutex, &dcp->hpd_mutex) { WRITE_ONCE(dcp->typec_cable_connected, false); dcp->typec_generation++; @@ -1778,10 +1898,24 @@ static void dcp_external_stop_work(struct work_struct *work) bool dcp_external_hotplug(struct apple_dcp *dcp, bool connected) { - if (!dcp_is_external_h17p(dcp)) + if (!dcp_is_retained_external(dcp)) return false; /* No lifecycle or HPD mutex may be acquired from the RX callback. */ lockdep_assert_held(&dcp->iomfb.lock); + if (dcp_is_ext26(dcp)) { + if (READ_ONCE(dcp->ext26_pm_preparing) || READ_ONCE(dcp->ext26_stopping) || + READ_ONCE(dcp->quiescing) || + READ_ONCE(dcp->external_physically_absent)) + return true; + if (connected) + return false; + if (READ_ONCE(dcp->external_stopped)) + return true; + dcp->external_stop_generation = dcp->external_rx_generation; + WRITE_ONCE(dcp->ext26_normal, false); + schedule_work(&dcp->external_stop_work); + return true; + } if (READ_ONCE(dcp->quiescing) || READ_ONCE(dcp->external_physically_absent)) return true; if (dcp->iomfb.presentation_paused) { @@ -1970,7 +2104,7 @@ bool dcp_hold_resume_event(struct platform_device *pdev) struct apple_dcp *dcp = platform_get_drvdata(pdev); struct apple_connector *con = READ_ONCE(dcp->connector); - if (!dcp_is_external_h17p(dcp) || !con || READ_ONCE(con->dcp) != pdev) + if (!dcp_is_retained_external(dcp) || !con || READ_ONCE(con->dcp) != pdev) return false; guard(mutex)(&dcp->hpd_mutex); @@ -2030,7 +2164,7 @@ static bool dcp_typec_relink_eligible(struct apple_dcp *dcp) /* H17P and native external pipes recover through their own lifecycle */ if (dcp->fw_compat == DCP_FIRMWARE_H17P || dcp->external_native || !dcp_is_typec_output(dcp) || dcp_is_usb4_output(dcp) || - dcp_is_external_h17p(dcp) || !dcp->typec_connector) + dcp_is_retained_external(dcp) || !dcp->typec_connector) return false; /* * Only a sink that is attached, shown and still connected for DRM: @@ -2187,7 +2321,7 @@ int dcp_dptx_disconnect(struct apple_dcp *dcp, u32 port) { int ret; - if (dcp_is_external_h17p(dcp)) + if (dcp_is_retained_external(dcp)) return dcp_external_disconnect(dcp, false); /* Release the caller's RemotePort service, not the downstream DFP port. */ dev_info(dcp->dev, "%s(port=%d)\n", __func__, port); @@ -2205,7 +2339,7 @@ int dcp_dptx_connect_oob(struct platform_device *pdev, u32 port) { struct apple_dcp *dcp = platform_get_drvdata(pdev); - if (dcp_is_external_h17p(dcp)) { + if (dcp_is_retained_external(dcp)) { bool new_cable; cancel_delayed_work_sync(&dcp->typec_reconnect_wq); @@ -2246,7 +2380,7 @@ int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port) { struct apple_dcp *dcp = platform_get_drvdata(pdev); - if (dcp_is_external_h17p(dcp)) + if (dcp_is_retained_external(dcp)) return dcp_external_disconnect(dcp, true); if (dcp_is_typec_output(dcp)) { @@ -2294,7 +2428,7 @@ int dcp_dptx_recover_irq(struct apple_dcp *dcp) int ret; mutex_lock(&dcp->hpd_mutex); - if (dcp->external_native || dcp_is_external_h17p(dcp) || + if (dcp->external_native || dcp_is_retained_external(dcp) || !dcp_is_typec_output(dcp) || dcp_is_usb4_output(dcp) || !dcp->typec_connector || READ_ONCE(dcp->typec_connector->connected) || @@ -2420,6 +2554,78 @@ unsigned long* dcp_get_iomfb_surfaces(struct platform_device *pdev) return dcp->iomfb_surfaces; } +static int ext26_aux_call(struct apple_epic_service *service, u16 group, u32 command, + const void *data, size_t size, void *reply, size_t room) +{ + memset(reply, 0, room); + memcpy(reply, data, min(size, room)); + if (room >= sizeof(u32)) + put_unaligned_le32(0, reply); + return 0; +} + +static const struct apple_epic_service_ops ext26_aux_ops[] = { + { .name = "native26-observer", .call_v2 = ext26_aux_call }, {} +}; + +void dcp_ext26_open_services(struct apple_dcp *dcp) +{ + struct apple_dcp_afkep *eps[] = { dcp->systemep, dcp->ibootep, + dcp->dptx_controller_ep, dcp->ext26_aux[0], dcp->dcpavservep, + dcp->ext26_aux[1], dcp->dptxep, dcp->ext26_aux[2] }; + unsigned int i; + + WRITE_ONCE(dcp->ext26_services_open, true); + for (i = 0; i < ARRAY_SIZE(eps); i++) + if (!IS_ERR_OR_NULL(eps[i])) + afk_service_retry(eps[i]); +} + +static int dcp_ext26_start_services(struct apple_dcp *dcp) +{ + static const u8 auxiliary[] = { 0x27, 0x29, 0x2c }; + unsigned int i; + int ret; + + ret = iomfb_start_rtkit(dcp); + if (ret) + return ret; + if (!wait_for_completion_timeout(&dcp->ext26_link_ready, msecs_to_jiffies(5000))) + return dev_err_probe(dcp->dev, -ETIMEDOUT, "native26 DCPLink readiness timed out\n"); + if (READ_ONCE(dcp->crashed)) + return -EIO; + ret = systemep_init(dcp); + if (!ret) + ret = ibootep_init(dcp); + if (!ret) + ret = dptx_controller_init(dcp); + for (i = 0; !ret && i < ARRAY_SIZE(auxiliary); i++) { + struct apple_dcp_afkep *ep; + + if (i == 1) { + ret = dpavservep_init(dcp); + if (ret) + break; + } + if (i == 2) { + ret = dptxep_init(dcp); + if (ret) + break; + } + ep = afk_init(dcp, auxiliary[i], ext26_aux_ops); + if (IS_ERR(ep)) + return PTR_ERR(ep); + ep->match_epic_name = false; + dcp->ext26_aux[i] = ep; + ret = afk_start(ep); + } + if (ret) + return dev_err_probe(dcp->dev, ret, "native26 AFK startup failed\n"); + dev_dbg(dcp->dev, "native26 AFK ready: 20 23 24 27 28 29 2a 2c; starting A401\n"); + iomfb_ext26_start(dcp); + return 0; +} + int dcp_start(struct platform_device *pdev) { struct apple_dcp *dcp = platform_get_drvdata(pdev); @@ -2450,6 +2656,8 @@ int dcp_start(struct platform_device *pdev) if (IS_ERR_OR_NULL(dcp->rtk)) return -ENODEV; + if (dcp_is_ext26(dcp)) + return dcp_ext26_start_services(dcp); /* Make the H17P allocator available before external service discovery. */ if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) { @@ -2470,14 +2678,14 @@ int dcp_start(struct platform_device *pdev) ret); } - if (dcp->phy && dcp->fw_compat >= DCP_FIRMWARE_V_13_5) { + if ((dcp->phy || dcp_is_ext26(dcp)) && dcp->fw_compat >= DCP_FIRMWARE_V_13_5) { ret = ibootep_init(dcp); if (ret) dev_warn(dcp->dev, "Failed to start IBOOT endpoint: %d\n", ret); if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL) { + DCP_IOMFB_METHODS_H17P_EXTERNAL || dcp_is_ext26(dcp)) { ret = dptx_controller_init(dcp); if (ret) return dev_err_probe(dcp->dev, ret, @@ -2522,7 +2730,7 @@ int dcp_start(struct platform_device *pdev) } #if IS_ENABLED(CONFIG_DRM_APPLE_AUDIO) - if (hdmi_audio) { + if (hdmi_audio && !dcp_is_ext26(dcp)) { ret = avep_init(dcp); if (ret) dev_warn(dcp->dev, "Failed to start AV endpoint: %d", ret); @@ -2747,7 +2955,9 @@ void dcp_poweron(struct platform_device *pdev) unsigned long remaining; int ret; - if (dcp_is_external_h17p(dcp)) { + if (dcp_is_ext26(dcp)) + flush_work(&dcp->ext26_blank_work); + if (dcp_is_retained_external(dcp)) { cancel_delayed_work_sync(&dcp->typec_reconnect_wq); cancel_delayed_work_sync(&dcp->placeholder_edid_wq); cancel_work_sync(&dcp->external_stop_work); @@ -2885,7 +3095,7 @@ void dcp_external_recover(struct platform_device *pdev) { struct apple_dcp *dcp = platform_get_drvdata(pdev); - if (dcp_is_external_h17p(dcp)) + if (dcp_is_retained_external(dcp)) dcp_poweron(pdev); } @@ -2895,7 +3105,12 @@ void dcp_poweroff(struct platform_device *pdev) bool display_gone = false; int ret; - if (dcp_is_external_h17p(dcp)) { + if (dcp_is_ext26(dcp)) { + if (!READ_ONCE(dcp->ext26_pm_preparing) && !READ_ONCE(dcp->external_stopped)) + queue_work(system_unbound_wq, &dcp->ext26_blank_work); + return; + } + if (dcp_is_retained_external(dcp)) { cancel_delayed_work_sync(&dcp->typec_reconnect_wq); cancel_delayed_work_sync(&dcp->placeholder_edid_wq); guard(mutex)(&dcp->external_lock); @@ -3433,6 +3648,27 @@ static int dcp_read_fw_version(struct device *dev, const char *name, return len; } +static bool dcp_ext26_admitted(struct device_node *np) +{ + struct device_node *chosen = of_find_node_by_path("/chosen"); + const char *opt, *uuid; + int length = 0; + u32 version[3], marker; + bool ok; + + opt = chosen ? of_get_property(chosen, "asahi,j613-25g83-dcpext", &length) : NULL; + ok = opt && length == 2 && opt[0] == '1' && !opt[1] && + of_machine_is_compatible("apple,j613") && + !of_property_read_u32(np, "apple,j613-25g83-dcpext-handoff", &marker) && marker == 1 && + !of_property_read_u32(np, "apple,t8122-dcpext-firmware-ready", &marker) && marker == 1 && + of_property_count_u32_elems(np, "apple,firmware-compat") == 3 && + !of_property_read_u32_array(np, "apple,firmware-compat", version, 3) && + !of_property_read_string(np, "apple,firmware-uuid", &uuid) && + j613_25g83_identity(true, 1, version, 3, uuid); + of_node_put(chosen); + return ok; +} + static enum dcp_firmware_version dcp_check_firmware_version(struct device *dev) { const struct apple_dcp_hw_data *hw = of_device_get_match_data(dev); @@ -3491,6 +3727,8 @@ static enum dcp_firmware_version dcp_check_firmware_version(struct device *dev) * firmware through the classic IOMFB template, as on M1/M2. */ if (of_device_is_compatible(dev->of_node, "apple,t8122-dcpext")) { + if (dcp_ext26_admitted(dev->of_node)) + return DCP_FIRMWARE_H17P; if (!strcmp(compat_str, "14.7.0")) return DCP_FIRMWARE_V_14_7_TEMPLATE; dev_err(dev, "T8122 external display not started: DCP firmware-compat %s is not 14.7.0\n", @@ -3574,7 +3812,7 @@ VISIBLE_IF_KUNIT void dcp_disable_typec_work(struct apple_dcp *dcp, bool release /* Nothing arms the timeout work past its drain below. */ dcp->announce.stopped = true; } - if (release_cable && dcp_is_external_h17p(dcp)) + if (release_cable && dcp_is_retained_external(dcp)) dpavservep_detach(dcp); /* Block new enqueues as well as draining users of the AFK endpoints. */ disable_delayed_work_sync(&dcp->typec_reconnect_wq); @@ -3606,7 +3844,7 @@ VISIBLE_IF_KUNIT void dcp_enable_typec_work(struct apple_dcp *dcp) /* A cable can be routed before the DRM component binds. */ if (READ_ONCE(dcp->typec_cable_connected)) dcp_queue_typec_reconnect(dcp, 0); - if (dcp_is_external_h17p(dcp)) { + if (dcp_is_retained_external(dcp)) { struct apple_connector *con = READ_ONCE(dcp->connector); guard(mutex)(&dcp->hpd_mutex); @@ -3683,7 +3921,7 @@ static int dcp_ext_wait_pmp(struct apple_dcp *dcp) unsigned long deadline; int ret; - if (!dcp_ext_dptx_607(dcp)) + if (!dcp_ext_dptx_607(dcp) && !dcp_is_ext26(dcp)) return 0; entry = of_parse_phandle(dcp->dev->of_node, "apple,pmp-report", 0); if (!entry) { @@ -3725,6 +3963,37 @@ static bool dcp_panel_is_j313(const struct dcp_panel_id *id, return id && !strcmp(id->name, "J313"); } +/* One initial resident session; reconnect and PM never call this routine. */ +static int dcp_ext26_start_resident(struct apple_dcp *dcp) +{ + struct device *dev = dcp->dev; + u32 control; + int ret; + + ret = dcp_pin_live_session(dcp); + if (ret) + return ret; + control = readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL); + dcp->rtk = apple_rtkit_init(dev, dcp, "mbox", 0, &rtkit_ops); + if (IS_ERR(dcp->rtk)) + return dev_err_probe(dev, PTR_ERR(dcp->rtk), "native26 RTKit mailbox initialization failed\n"); + ret = devm_add_action_or_reset(dev, dcp_release_rtkit, dcp); + if (ret) + return ret; + if (!(control & APPLE_DCP_COPROC_CPU_CONTROL_RUN)) { + writel(control | APPLE_DCP_COPROC_CPU_CONTROL_RUN, + dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL); + dev_dbg(dev, "native26 resident ASC started once after RTKit mailbox initialization\n"); + } + ret = control & APPLE_DCP_COPROC_CPU_CONTROL_RUN ? + apple_rtkit_wake(dcp->rtk) : apple_rtkit_boot(dcp->rtk); + dev_dbg(dev, "native26 initial RTKit %s result=%d control=%#x status=%#x\n", + control & APPLE_DCP_COPROC_CPU_CONTROL_RUN ? "warm INIT" : "resident boot", + ret, readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL), + readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_STATUS)); + return ret; +} + /* * The boot loader hands a panel over lit as an available simple-framebuffer * whose "panel" points at it, until the first power-on has cycled it (see @@ -3788,7 +4057,7 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat return ret; /* A native external processor mapped these at probe, for its start work. */ - if (!dcp->external_native) { + if (!dcp->external_native && !dcp_is_ext26(dcp)) { dcp->coproc_reg = devm_platform_ioremap_resource_byname(to_platform_device(dev), "coproc"); if (IS_ERR(dcp->coproc_reg)) @@ -3804,7 +4073,11 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat if (dcp->hw.adopt_live_session) { cpu_ctrl = readl_relaxed(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL); - if (!(cpu_ctrl & APPLE_DCP_COPROC_CPU_CONTROL_RUN)) + if (dcp_is_ext26(dcp)) + dev_dbg(dev, "native26 initial carrier: control=%#x status=%#x; RTKit %s\n", + cpu_ctrl, readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_STATUS), + cpu_ctrl & APPLE_DCP_COPROC_CPU_CONTROL_RUN ? "warm INIT" : "resident first boot"); + if (!(cpu_ctrl & APPLE_DCP_COPROC_CPU_CONTROL_RUN) && !dcp_is_ext26(dcp)) return dev_err_probe(dev, -EOPNOTSUPP, "DCP was not left running by the bootloader\n"); } @@ -3976,6 +4249,13 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat if (ret) goto err_piodma; + if (dcp_is_ext26(dcp)) { + ret = dcp_ext26_start_resident(dcp); + if (ret) + goto err_piodma; + goto session_ready; + } + if (!dcp->hw.adopt_live_session) { cpu_ctrl = readl_relaxed(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL); @@ -4016,6 +4296,7 @@ static int dcp_comp_bind_pipe(struct device *dev, struct device *main, void *dat ret = dev_err_probe(dev, ret, "Failed to boot RTKit: %d\n", ret); goto err_piodma; } +session_ready: enable_work(&dcp->dimensions_wq); schedule_work(&dcp->dimensions_wq); dcp_enable_typec_work(dcp); @@ -4090,6 +4371,8 @@ VISIBLE_IF_KUNIT void dcp_comp_unbind(struct device *dev, struct device *main, v return; } + if (dcp_is_ext26(dcp)) + cancel_work_sync(&dcp->ext26_blank_work); if (dcp->retain_dma) { /* No firmware stop is acknowledged: detach host users, never DMA. */ scoped_guard(mutex, &dcp->iomfb.lock) @@ -4349,6 +4632,23 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) mutex_init(&dcp->dcpavserv.lock); INIT_WORK(&dcp->dcpavserv.close_wq, dcpavserv_close_work); dcp->hw = *(struct apple_dcp_hw_data *)of_device_get_match_data(dev); + if (of_device_is_compatible(dev->of_node, "apple,t8122-dcpext") && + fw_compat == DCP_FIRMWARE_H17P) { + dcp->hw.iomfb_method_profile = DCP_IOMFB_METHODS_J613_EXT26; + dcp->hw.adopt_live_session = true; + dcp->hw.fw_14_7_endpoints = false; + init_completion(&dcp->ext26_link_ready); + init_completion(&dcp->ext26_ready); + INIT_WORK(&dcp->ext26_blank_work, dcp_ext26_blank_work); + INIT_LIST_HEAD(&dcp->ext26_retained); + dcp->coproc_reg = devm_platform_ioremap_resource_byname(pdev, "coproc"); + if (IS_ERR(dcp->coproc_reg)) + return PTR_ERR(dcp->coproc_reg); + dev_dbg(dev, "native26 probe carrier: control=%#x status=%#x; firmware CPU lease supplied by loader\n", + readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL), + readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_STATUS)); + } + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) { if (!dcp->hw.firmware_clock || @@ -4457,6 +4757,11 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) set_bit(surf, dcp->iomfb_surfaces); surf++; } + if (dcp_is_ext26(dcp)) { + bitmap_zero(dcp->iomfb_surfaces, DCP_MAX_PLANES); + set_bit(0, dcp->iomfb_surfaces); + } + /* The J700 loader names surface zero rather than enabling it. */ if (of_machine_is_compatible("apple,j700") && of_device_is_compatible(dev->of_node, "apple,t8140-dcp") && @@ -4618,8 +4923,7 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) return iomfb_v14_7_external_start(dcp); } - if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL) { + if (dcp_is_retained_external(dcp)) { /* Complete optional startup before the DRM master can depend on it. */ ret = dcp_comp_bind(dev, NULL, NULL); if (ret) { @@ -4954,6 +5258,15 @@ static int dcp_platform_prepare(struct device *dev) struct apple_dcp *dcp = dev_get_drvdata(dev); const char *why; + if (dcp_is_ext26(dcp)) { + if (READ_ONCE(dcp->crashed) || READ_ONCE(dcp->ext26_blocked)) + return -EIO; + WRITE_ONCE(dcp->ext26_pm_preparing, true); + cancel_work_sync(&dcp->ext26_blank_work); + dcp_disable_typec_work(dcp, false); + dcp->ext26_pm_work_off = true; + return 0; + } if (!dcp->external) return 0; mutex_lock(&dcp->hpd_mutex); @@ -4975,6 +5288,16 @@ static void dcp_platform_complete(struct device *dev) { struct apple_dcp *dcp = dev_get_drvdata(dev); + if (dcp_is_ext26(dcp)) { + WRITE_ONCE(dcp->ext26_pm_preparing, false); + if (dcp->ext26_pm_work_off && !READ_ONCE(dcp->ext26_blocked) && + !READ_ONCE(dcp->crashed)) { + dcp_enable_typec_work(dcp); + dcp->ext26_pm_work_off = false; + } + return; + } + if (!dcp->external) return; mutex_lock(&dcp->hpd_mutex); @@ -4986,6 +5309,10 @@ static int dcp_platform_suspend(struct device *dev) { struct apple_dcp *dcp = dev_get_drvdata(dev); + if (dcp_is_ext26(dcp) && pm_suspend_target_state != PM_SUSPEND_TO_IDLE) { + dev_warn(dev, "native26 refuses non-s2idle target %d\n", pm_suspend_target_state); + return -EBUSY; + } if (READ_ONCE(dcp->ext_dead)) return 0; iomfb_v14_7_backlight_suspend(dcp); @@ -5011,16 +5338,23 @@ static int dcp_platform_suspend(struct device *dev) mutex_unlock(&dcp->hpd_mutex); } - if (dcp_is_external_h17p(dcp)) { + if (dcp_is_retained_external(dcp)) { int ret; - dcp_disable_typec_work(dcp, false); + if (!dcp_is_ext26(dcp)) + dcp_disable_typec_work(dcp, false); ret = dcp_external_disconnect(dcp, false); if (ret) { - dcp_enable_typec_work(dcp); + if (!dcp_is_ext26(dcp)) + dcp_enable_typec_work(dcp); return ret; } + if (dcp_is_ext26(dcp) && + !(readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL) & APPLE_DCP_COPROC_CPU_CONTROL_RUN)) + return -EHOSTDOWN; device_set_wakeup_path(dev); + if (dcp_is_ext26(dcp)) + dev_dbg(dev, "native26 retained s2idle stop; firmware and supplier leases held\n"); return 0; } @@ -5065,6 +5399,19 @@ static int dcp_platform_resume(struct device *dev) if (READ_ONCE(dcp->ext_dead)) return 0; + if (dcp_is_ext26(dcp)) { + if (!(readl(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL) & APPLE_DCP_COPROC_CPU_CONTROL_RUN) || + READ_ONCE(dcp->crashed)) { + WRITE_ONCE(dcp->ext26_blocked, true); + WRITE_ONCE(dcp->external_error, -EHOSTDOWN); + dev_err(dev, "native26 firmware did not survive s2idle; retained session requires reboot\n"); + return -EHOSTDOWN; + } + WRITE_ONCE(dcp->external_suspended, false); + /* Complete restarts discovery after DRM has dropped the stale CRTC. */ + dev_dbg(dev, "native26 resumed retained firmware; discovery waits for PM complete\n"); + return 0; + } dcp_release_after_sleep(dcp); iomfb_v14_7_backlight_resume(dcp); dcp_enable_typec_work(dcp); diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index baff04ee201e72..9789abd0ca010e 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -66,6 +66,8 @@ bool dcp_fw_compat_is_14_7(struct platform_device *pdev); bool dcp_ext_unusable(struct platform_device *pdev); unsigned long* dcp_get_iomfb_surfaces(struct platform_device *pdev); int dcp_start(struct platform_device *pdev); +void dcp_ext26_open_services(struct apple_dcp *dcp); +int dcp_ext26_layout_stop(struct apple_dcp *dcp); int dcp_wait_ready(struct platform_device *pdev, u64 timeout); void dcp_flush(struct drm_crtc *crtc, struct drm_atomic_commit *commit); void dcp_retain_framebuffer(struct platform_device *pdev, @@ -109,6 +111,7 @@ int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port); bool dcp_hold_resume_event(struct platform_device *pdev); int iomfb_start_rtkit(struct apple_dcp *dcp); +void iomfb_ext26_start(struct apple_dcp *dcp); void iomfb_shutdown(struct apple_dcp *dcp); /* rtkit message handler for IOMFB messages */ void iomfb_recv_msg(struct apple_dcp *dcp, u64 message); diff --git a/drivers/gpu/drm/apple/dcpext26-lifecycle.h b/drivers/gpu/drm/apple/dcpext26-lifecycle.h new file mode 100644 index 00000000000000..4eb85f49e22e98 --- /dev/null +++ b/drivers/gpu/drm/apple/dcpext26-lifecycle.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef APPLE_DCPEXT26_LIFECYCLE_H +#define APPLE_DCPEXT26_LIFECYCLE_H + +#include + +/* A protocol control completion grants no permission to retire DMA backing. */ +struct dcpext26_exchange { + u32 id; + bool replied; + bool completed; + bool failed; +}; + +static inline bool dcpext26_exchange_begin(struct dcpext26_exchange *x, u32 id) +{ + if (!id || x->id) + return false; + x->id = id; + return true; +} + +static inline bool dcpext26_exchange_complete(struct dcpext26_exchange *x, u32 id) +{ + if (!x->id || id != x->id) + return false; + x->completed = true; + return true; +} + +static inline bool dcpext26_exchange_finished(const struct dcpext26_exchange *x) +{ + return x->failed || (x->id && x->replied && x->completed); +} +/* Reserve headroom for an in-flight full-frame primary before readmission. */ +static inline bool dcpext26_epoch_fits(unsigned int epochs, u64 retained, u64 incoming) +{ + const u64 limit = (1ULL << 30) - (64ULL << 20); + + return !incoming || (epochs < 8 && retained <= limit && incoming <= limit - retained); +} +#endif diff --git a/drivers/gpu/drm/apple/dcpext26-wire.h b/drivers/gpu/drm/apple/dcpext26-wire.h new file mode 100644 index 00000000000000..2fa54054260d3f --- /dev/null +++ b/drivers/gpu/drm/apple/dcpext26-wire.h @@ -0,0 +1,122 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef APPLE_DCPEXT26_WIRE_H +#define APPLE_DCPEXT26_WIRE_H + +#include +#include +#include +#include + +/* J613 26.6.2 external personality; neither classic 14.7 nor H17P. */ +#define DCPEXT26_SUBMIT_BYTES 0x1bd8 +#define DCPEXT26_SURFACE_BYTES 0x22c + +struct dcpext26_primary { + u64 address; + u64 backing_bytes; + u32 width; + u32 height; + u32 pitch; +}; + +/* Validate all arithmetic before changing a caller-owned command buffer. */ +static inline int dcpext26_primary_check(const struct dcpext26_primary *p) +{ + u64 bytes = (u64)p->pitch * p->height; + + if (!p->address || !p->width || !p->height || + (p->pitch & 3) || (u64)p->width * 4 > p->pitch) + return -EINVAL; + if (bytes > (u32)~0U || bytes > p->backing_bytes || + p->address > (u64)~0ULL - bytes) + return -ERANGE; + return 0; +} + +/* Full-frame linear opaque BGRA. All unused layers stay explicitly null. */ +static inline int dcpext26_primary_encode(void *buffer, size_t capacity, + u32 id, unsigned int layer, + const struct dcpext26_primary *p) +{ + u8 *wire = buffer, *surface; + u32 bytes; + unsigned int i; + int ret; + + if (!buffer || capacity != DCPEXT26_SUBMIT_BYTES || !id || layer >= 4) + return -EINVAL; + if (p) { + ret = dcpext26_primary_check(p); + if (ret) + return ret; + } + memset(wire, 0, capacity); + put_unaligned_le32(id, wire + 0x98); + put_unaligned_le32(0x80000007, wire + 0x14c); + put_unaligned_le32(0x80000007, wire + 0x150); + put_unaligned_le32(0xff000000, wire + 0x154); + /* Ten nullable surfaces, followed by the two output-pointer flags. */ + memset(wire + 0x1bcb, 1, 10); + wire[0x1bd6] = 1; + wire[0x1bd7] = 1; + if (!p) + return 0; + + surface = wire + 0x588 + layer * DCPEXT26_SURFACE_BYTES; + bytes = p->pitch * p->height; + { + const struct { unsigned int offset; u32 value; } words[] = { + {0x21, p->width}, {0x59, p->width}, + {0x25, p->height}, {0x5d, p->height}, + {0x15, p->pitch}, {0x69, p->pitch}, + {0x29, bytes}, {0x6d, bytes}, + {3, 1}, {7, 1}, {0xb, 0x42475241}, {0x35, 1}, + }; + const struct { unsigned int offset; u8 value; } octets[] = { + {2, 1}, {0x13, 13}, {0x14, 12}, {0x19, 1}, + {0x1b, 1}, {0x1c, 1}, {0x71, 4}, {0x73, 1}, {0x74, 1}, + }; + + for (i = 0; i < sizeof(words) / sizeof(words[0]); i++) + put_unaligned_le32(words[i].value, surface + words[i].offset); + for (i = 0; i < sizeof(octets) / sizeof(octets[0]); i++) + surface[octets[i].offset] = octets[i].value; + put_unaligned_le64(1, surface + 0x149); + put_unaligned_le64(1, surface + 0x51); + } + put_unaligned_le64(p->address, wire + 0xe38 + layer * 8); + put_unaligned_le32(1, wire + 0x9c + layer * 4); + for (i = 0; i < 2; i++) { + u8 *rect = wire + (i ? 0x10c : 0xac) + layer * 16; + + put_unaligned_le32(p->width, rect + 8); + put_unaligned_le32(p->height, rect + 12); + } + wire[0x1bcb + layer] = 0; + return 0; +} + +static inline int dcpext26_start_reply(const void *reply, size_t length, u32 *id) +{ + const u8 *wire = reply; + u32 value; + + if (!reply || !id || length != 8) + return -EINVAL; + value = get_unaligned_le32(wire); + if (!value || get_unaligned_le32(wire + 4)) + return -EPROTO; + *id = value; + return 0; +} + +static inline int dcpext26_submit_reply(const void *reply, size_t length) +{ + const u8 *wire = reply; + + if (!reply || length != 12) + return -EINVAL; + return get_unaligned_le32(wire + 5) ? -EPROTO : 0; +} + +#endif diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 741f40bc8c9ed5..517e0864f8791a 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -569,7 +569,8 @@ static int dptxport_call_set_link_rate(struct apple_epic_service *service, } } else if (dptx->atcphy) { /* No clocked crossbar output across a PHY rate change. */ - dcp_direct_crossbar_link(service->ep->dcp, false); + if (!dcp_is_ext26(service->ep->dcp)) + dcp_direct_crossbar_link(service->ep->dcp, false); dptx->phy_ops.dp.link_rate = phy_link_rate; dptx->phy_ops.dp.set_rate = 1; ret = phy_configure(dptx->atcphy, &dptx->phy_ops); @@ -916,7 +917,7 @@ static int dptxport_prepare(struct apple_epic_service *service, const char *name * T6030 14.7 also announces the low-power port service, and may leave * its class empty. */ - if (dcp_fw_is_14_7(dcp) && name && + if ((dcp_fw_is_14_7(dcp) || dcp_is_ext26(dcp)) && name && (!strcmp(name, "dcpdptx-port-epic") || !strcmp(name, "dcp-lpdptx-port-epic")) && (!class || !class[0] || !strcmp(class, "AppleDCPDPTXRemotePort"))) @@ -942,8 +943,8 @@ static void dptxport_init(struct apple_epic_service *service, const char *name, if (service->ep->dcp->external_native) schedule_work(&service->ep->dcp->external_ready_work); /* Publication may follow the startup wait or the bounded retry window. */ - if (service->ep->dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL && + if ((service->ep->dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL || dcp_is_ext26(service->ep->dcp)) && READ_ONCE(service->ep->dcp->typec_cable_connected) && !READ_ONCE(service->ep->dcp->quiescing) && !READ_ONCE(service->ep->dcp->external_error)) @@ -951,9 +952,140 @@ static void dptxport_init(struct apple_epic_service *service, const char *name, &service->ep->dcp->typec_reconnect_wq, 0); } +/* Native26 USB-C callbacks use the current Type-C PHY without changing its mode. */ +static int dptxport_native26_call(struct apple_epic_service *service, u16 group, u32 command, + const void *data, size_t size, void *reply, size_t room) +{ + struct dptx_port *port = service->cookie; + struct apple_dcp *dcp = service->ep->dcp; + union phy_configure_opts capacity = {}; + int ret; + + if (group == 1 && (command == 3 || command == 4)) + return dptx_j700_training(service, command == 3, data, size, reply, room); + if (group || (size && !data)) + return -EOPNOTSUPP; + memset(reply, 0, room); + memcpy(reply, data, min(size, room)); + if (room >= 4) + put_unaligned_le32(0, reply); + switch (command) { + case DPTX_APCALL_ACTIVATE: + if (!port->atcphy) + return -ENODEV; + ret = phy_validate(port->atcphy, PHY_MODE_DP, 0, &capacity); + if (ret || capacity.dp.lanes < 2) + return ret ?: -EINVAL; + return dcp_direct_crossbar_link(dcp, true); + case DPTX_APCALL_DEACTIVATE: + return dcp_direct_crossbar_link(dcp, false); + case DPTX_APCALL_WILL_CHANGE_LINKG_CONFIG: + memset(reply, 0, room); + return dcp_direct_crossbar_link(dcp, false); + case DPTX_APCALL_DID_CHANGE_LINK_CONFIG: + memset(reply, 0, room); + mdelay(10); + return port->link_rate ? dcp_direct_crossbar_link(dcp, true) : 0; + case DPTX_APCALL_GET_MAX_DRIVE_SETTINGS: + if (room < 28) + return -EINVAL; + memset(reply, 0, room); + put_unaligned_le32(3, reply + 20); + put_unaligned_le32(3, reply + 24); + return 0; + case DPTX_APCALL_SET_DRIVE_SETTINGS: + return -EOPNOTSUPP; + case DPTX_APCALL_GET_DRIVE_SETTINGS: + return dptx_j700_training(service, false, data, size, reply, room); + case DPTX_APCALL_GET_MAX_LINK_RATE: + if (room < 32) + return -EINVAL; + memset(reply, 0, room); + put_unaligned_le32(LINK_RATE_HBR2, reply + 16); + return 0; + case DPTX_APCALL_GET_LINK_RATE: + if (room < 32) + return -EINVAL; + memset(reply, 0, room); + put_unaligned_le32(port->link_rate, reply + 16); + return 0; + case DPTX_APCALL_SET_LINK_RATE: + if (size < 32 || room < 32) + return -EINVAL; + switch (get_unaligned_le32(data + 16)) { + case 0: + case LINK_RATE_RBR: + case LINK_RATE_HBR: + case LINK_RATE_HBR2: + case LINK_RATE_HBR3: + break; + default: + return -EINVAL; + } + ret = dcp_direct_crossbar_link(dcp, false); + if (ret) + return ret; + return dptxport_call_set_link_rate(service, data, size, reply, room); + case DPTX_APCALL_GET_MAX_LANE_COUNT: + if (room < 32 || !port->atcphy) + return -EINVAL; + ret = phy_validate(port->atcphy, PHY_MODE_DP, 0, &capacity); + if (ret || capacity.dp.lanes < 2 || capacity.dp.lanes > 4) + return ret ?: -EINVAL; + port->native26_max_lanes = capacity.dp.lanes; + memset(reply, 0, room); + put_unaligned_le64(capacity.dp.lanes, reply + 16); + return 0; + case DPTX_APCALL_SET_ACTIVE_LANE_COUNT: { + u64 lanes; + + if (size < 32 || room < 32) + return -EINVAL; + lanes = get_unaligned_le64(data + 16); + ret = dptx_j700_validate_lanes(port, lanes); + if (ret) + return ret; + port->lane_count = lanes; + put_unaligned_le64(lanes, reply + 16); + if (lanes) + complete(&port->linkcfg_completion); + return 0; + } + case DPTX_APCALL_GET_ACTIVE_LANE_COUNT: + if (room < 32) + return -EINVAL; + memset(reply, 0, room); + put_unaligned_le64(port->lane_count, reply + 16); + return 0; + case DPTX_APCALL_GET_SUPPORTS_HPD: + if (room < 32) + return -EINVAL; + memset(reply, 0, room); + put_unaligned_le32(1, reply + 16); + return 0; + case DPTX_APCALL_GET_SUPPORTS_DOWN_SPREAD: + if (room < 32) + return -EINVAL; + memset(reply, 0, room); + return 0; + case DPTX_APCALL_SET_TILED_DISPLAY_HINTS: + if (room < 4) + return -EINVAL; + put_unaligned_le32(1, reply); + return 0; + case DPTX_APCALL_GET_VIRTUAL_DEVICE_MODE: + memset(reply, 0, room); + return 0; + default: + return dptxport_call(service, command, data, size, reply, room); + } +} + static int dptxport_v2_group_call(struct apple_epic_service *service, u16 group, u32 command, const void *data, size_t size, void *reply, size_t room) { + if (dcp_is_ext26(service->ep->dcp)) + return dptxport_native26_call(service, group, command, data, size, reply, room); if (group == 1 && command == DPTX_APCALL_SET_DRIVE_SETTINGS) return dptx_j700_training(service, true, data, size, reply, room); if (group == 1 && command == DPTX_APCALL_GET_DRIVE_SETTINGS) @@ -1079,8 +1211,61 @@ static void dptx_controller_service_init(struct apple_epic_service *service, const char *name, const char *class, s64 unit) { complete(&service->ep->dcp->dptx_controller_ready); + if (dcp_is_ext26(service->ep->dcp) && + dptx_native_controllers_ready(service->ep->dcp)) { + dev_dbg(service->ep->dcp->dev, + "native26 all four controller interfaces OPEN; port OPEN enabled\n"); + afk_service_retry(service->ep->dcp->dptxep); + } +} + +bool dptx_native_controllers_ready(struct apple_dcp *dcp) +{ + struct apple_dcp_afkep *ep = READ_ONCE(dcp->dptx_controller_ep); + unsigned long flags, mask = 0; + u32 i; + + if (IS_ERR_OR_NULL(ep)) + return false; + for (i = 0; i < min_t(u32, READ_ONCE(ep->num_channels), AFK_MAX_CHANNEL); i++) { + struct apple_epic_service *service = &ep->services[i]; + + spin_lock_irqsave(&service->lock, flags); + if (service->enabled && !service->torndown) + mask |= (unsigned long)service->cookie; + spin_unlock_irqrestore(&service->lock, flags); + } + return mask == 0xf; } +static int dptx_native_controller_prepare(struct apple_epic_service *service, + const char *name, const char *class, s64 unit) +{ + unsigned int kind; + + if (!name || !class || strcmp(class, "AppleDCPDPTXController") || + unit < 0 || unit > 1) + return -EINVAL; + if (!strcmp(name, "dcpav-controller-epic")) + kind = 0; + else if (!strcmp(name, "dcpdp-controller-epic")) + kind = 1; + else + return -EINVAL; + /* A bit identifies each unit's AV and DP interface, independently of channel. */ + service->cookie = (void *)(unsigned long)BIT(unit * 2 + kind); + return 0; +} + +static const struct apple_epic_service_ops dptx_native_controller_ops[] = { + { + .name = "AppleDCPDPTXController", + .prepare = dptx_native_controller_prepare, + .init = dptx_controller_service_init, + }, + {} +}; + static const struct apple_epic_service_ops dptx_controller_ops[] = { { .name = "AppleDCPDPTXController", @@ -1095,17 +1280,20 @@ int dptx_controller_init(struct apple_dcp *dcp) int ret; init_completion(&dcp->dptx_controller_ready); - ep = afk_init(dcp, DPTX_CONTROLLER_ENDPOINT, dptx_controller_ops); + ep = afk_init(dcp, DPTX_CONTROLLER_ENDPOINT, + dcp_is_ext26(dcp) ? dptx_native_controller_ops : dptx_controller_ops); if (IS_ERR(ep)) return PTR_ERR(ep); /* Controller interfaces require the same OPEN handshake as the port. */ - ep->wire_v2 = dcp->hw.iomfb_method_profile == + ep->wire_v2 = dcp_is_ext26(dcp) || dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL; dcp->dptx_controller_ep = ep; ret = afk_start(ep); if (ret) return ret; + if (dcp_is_ext26(dcp)) + return 0; /* Native OPEN follows A401. */ if (!wait_for_completion_timeout(&dcp->dptx_controller_ready, msecs_to_jiffies(1000))) return -ETIMEDOUT; @@ -1116,7 +1304,7 @@ int dptx_controller_init(struct apple_dcp *dcp) int dptxep_init(struct apple_dcp *dcp) { struct apple_dcp_afkep *ep; - bool wire_v2 = dcp->hw.iomfb_method_profile == + bool wire_v2 = dcp_is_ext26(dcp) || dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL; int ret; u32 port; @@ -1127,7 +1315,7 @@ int dptxep_init(struct apple_dcp *dcp) init_completion(&dcp->dptxport[0].linkcfg_completion); init_completion(&dcp->dptxport[1].linkcfg_completion); - ep = afk_init(dcp, wire_v2 ? DPTX_ENDPOINT_V2 : DPTX_ENDPOINT, dptxep_ops); + ep = afk_init(dcp, wire_v2 && !dcp_is_ext26(dcp) ? DPTX_ENDPOINT_V2 : DPTX_ENDPOINT, dptxep_ops); if (IS_ERR(ep)) return PTR_ERR(ep); ep->wire_v2 = wire_v2; @@ -1137,6 +1325,8 @@ int dptxep_init(struct apple_dcp *dcp) if (ret) return ret; + if (dcp_is_ext26(dcp)) + return 0; /* Native OPEN follows A401; publication retries discovery. */ for (port = 0; port < dcp->hw.num_dptx_ports; port++) { ret = wait_for_completion_timeout(&dcp->dptxport[port].enable_completion, timeout); @@ -1147,7 +1337,7 @@ int dptxep_init(struct apple_dcp *dcp) * kIOReturnNoDevice (0xe00002c0). Leave the endpoint * up so a later announce can still bind. */ - if (dcp_fw_is_14_7(dcp)) { + if (dcp_fw_is_14_7(dcp) || dcp_is_ext26(dcp)) { dev_info(dcp->dev, "DPTX port %u not announced yet\n", port); return 0; diff --git a/drivers/gpu/drm/apple/dptxep.h b/drivers/gpu/drm/apple/dptxep.h index 211e72d9855ea8..b577d2ec8ba1af 100644 --- a/drivers/gpu/drm/apple/dptxep.h +++ b/drivers/gpu/drm/apple/dptxep.h @@ -60,6 +60,7 @@ struct dptx_port { struct phy *atcphy; struct mux_control *mux; u32 lane_count; + u32 native26_max_lanes; u32 link_rate, pending_link_rate; u32 drive_settings[2]; u32 trained_voltage[4], trained_pre[4]; @@ -84,5 +85,6 @@ int dptxport_set_hpd_timeout(struct apple_epic_service *service, bool hpd, int dptxport_sink_irq(struct apple_epic_service *service); struct apple_dcp; +bool dptx_native_controllers_ready(struct apple_dcp *dcp); int dpav_ctrl_init(struct apple_dcp *dcp); #endif diff --git a/drivers/gpu/drm/apple/epic/dpavservep.c b/drivers/gpu/drm/apple/epic/dpavservep.c index ed94c48c80d1a4..b551e2f0be5a29 100644 --- a/drivers/gpu/drm/apple/epic/dpavservep.c +++ b/drivers/gpu/drm/apple/epic/dpavservep.c @@ -39,6 +39,9 @@ static bool dcpavserv_identity(struct apple_epic_service *service, const char *n { if (!name || strcmp(name, "dcpav-service-epic")) return false; + if (dcp_is_ext26(service->ep->dcp)) + return service->ep->endpoint == DPAVSERV_ENDPOINT && unit == 0 && + class && !strcmp(class, "DCPDPService") && service->channel; if (!service->ep->wire_v2) return unit == 0; /* H17P's lower-case interface-id is dynamic, not a physical-port unit. */ @@ -134,6 +137,8 @@ static void dcpavserv_init(struct apple_epic_service *service, const char *name, mutex_unlock(&dcp->dcpavserv.lock); if (v2) mutex_unlock(&dcp->hpd_mutex); + if (published && dcp_is_ext26(dcp) && !dcp->ext26_client_open) + WRITE_ONCE(dcp->ext26_edid_tried, false); if (!published) { /* A generation/registry change during OPEN cannot enable stale work. */ afk_service_retire(service); @@ -567,8 +572,8 @@ static const struct drm_edid *dcpavserv_read_edid_inline(struct apple_epic_servi bool dcpavserv_edid_current(struct apple_dcp *dcp, u64 generation) { lockdep_assert_held(&dcp->hpd_mutex); - return dcp->hw.iomfb_method_profile != - DCP_IOMFB_METHODS_H17P_EXTERNAL || + return (!dcp_is_ext26(dcp) && dcp->hw.iomfb_method_profile != + DCP_IOMFB_METHODS_H17P_EXTERNAL) || (dcp->typec_cable_connected && generation == dcp->typec_generation && !READ_ONCE(dcp->quiescing) && !READ_ONCE(dcp->external_physically_absent)); } @@ -578,7 +583,7 @@ bool dcpavserv_edid_service_current(struct apple_dcp *dcp, u64 epoch) lockdep_assert_held(&dcp->dcpavserv.lock); return dcp->dcpavserv.enabled && dcp->dcpavserv.service && epoch == dcp->dcpavserv.epoch && - (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL || + ((!dcp_is_ext26(dcp) && dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL) || !READ_ONCE(dcp->external_physically_absent)); } @@ -641,7 +646,7 @@ const struct drm_edid *dcpavserv_copy_edid(struct apple_dcp *dcp, u64 generation edid = service->ep->wire_v2 ? dcpavserv_read_edid_inline(service) : dcpavserv_read_edid_legacy(service); /* The 14.7 external role: every block's header and checksum. */ - if (v14_7_ext && !IS_ERR(edid) && !drm_edid_valid(edid)) { + if ((v14_7_ext || dcp_is_ext26(dcp)) && !IS_ERR(edid) && !drm_edid_valid(edid)) { dev_warn(dcp->dev, "DCPAVSERV: the sink's EDID is not valid\n"); drm_edid_free(edid); edid = ERR_PTR(-EPROTO); @@ -700,7 +705,7 @@ int dpavservep_init(struct apple_dcp *dcp) if (IS_ERR(ep)) return PTR_ERR(ep); /* External H17P uses AFKV2 service calls with host-owned OOB buffers. */ - ep->wire_v2 = dcp->hw.iomfb_method_profile == + ep->wire_v2 = dcp_is_ext26(dcp) || dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL; ep->inline_v2 = false; dcp->dcpavservep = ep; diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 6e80eca48ad9ac..897d1520a33b97 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -36,6 +36,7 @@ #include "dcp-internal.h" #include "iomfb.h" #include "iomfb_internal.h" +#include "dcpext26-lifecycle.h" #include "parser.h" #include "trace.h" @@ -162,6 +163,8 @@ static u8 dcp_pop_depth(u8 *depth) } /* Older firmware and the H17G method profile retain their existing transport. */ +static u64 ext26_scanout_bytes(struct iomfb_scanout_h17p *scanout); + static bool iomfb_uses_queue(struct apple_dcp *dcp) { return dcp->fw_compat == DCP_FIRMWARE_H17P && @@ -197,7 +200,7 @@ static int iomfb_enqueue_opaque_x(struct apple_dcp *dcp) lockdep_assert_held(&dcp->iomfb.lock); /* DCPEXT rejects enableGPAlphaDiv; retain the panel-only workaround. */ if (dcp->hw.iomfb_method_profile == - DCP_IOMFB_METHODS_H17P_EXTERNAL) + DCP_IOMFB_METHODS_H17P_EXTERNAL || dcp_is_ext26(dcp)) return 0; if (atomic_read(&dcp->iomfb.opaque_x_state) != IOMFB_OPAQUE_X_NEEDED) return 0; @@ -243,7 +246,8 @@ static void iomfb_queue_advance(struct apple_dcp *dcp) /* The active operation may still be visible to firmware. */ return; } - if (!iomfb_channels_idle(dcp) || dcp->present_state_h17p.pending) + if (!iomfb_channels_idle(dcp) || dcp->present_state_h17p.pending || + dcp->ext26_control_pending) return; if (transaction) { @@ -648,6 +652,8 @@ void dcp_push(struct apple_dcp *dcp, bool oob, const struct dcp_method_entry *ca if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G && call->tag_h17g[0]) memcpy(resolved.tag, call->tag_h17g, sizeof(resolved.tag)); + if (dcp_is_ext26(dcp) && call->tag_ext26[0]) + memcpy(resolved.tag, call->tag_ext26, sizeof(resolved.tag)); call = &resolved; enum dcp_context_id context = dcp_call_context(dcp, oob); @@ -688,9 +694,9 @@ void dcp_push(struct apple_dcp *dcp, bool oob, const struct dcp_method_entry *ca if (in_len > 0) memcpy(out_data, data, in_len); - /* An unwritten status must not look like a successful response. */ + /* Native26 Boolean replies may leave their padding unwritten. */ if (out_len) - memset(out_data + in_len, 0xff, out_len); + memset(out_data + in_len, dcp_is_ext26(dcp) ? 0 : 0xff, out_len); trace_iomfb_push(dcp, call, context, offset, depth); @@ -700,8 +706,19 @@ void dcp_push(struct apple_dcp *dcp, bool oob, const struct dcp_method_entry *ca ch->header[depth] = header; ch->end[depth] = offset + ALIGN(data_len, DCP_PACKET_ALIGNMENT); + if (dcp_is_ext26(dcp)) + dma_wmb(); dcp_send_message(dcp, IOMFB_ENDPOINT, dcpep_msg(context, data_len, offset)); + /* Only a successful transport send can establish the readiness boundary. */ + if (dcp_is_ext26(dcp) && !memcmp(call->tag, "A473", 4) && in_len == 16 && + get_unaligned_le64(data) == 1) { + bool sent = !READ_ONCE(dcp->crashed) && !READ_ONCE(dcp->quiescing) && + !READ_ONCE(dcp->ext26_blocked); + + WRITE_ONCE(dcp->ext26_power_ns, sent ? ktime_get_ns() : 0); + WRITE_ONCE(dcp->ext26_power_sent, sent); + } } /* Parse a callback tag "D123" into the ID 123. Returns -EINVAL on failure. */ @@ -729,6 +746,8 @@ void dcp_ack(struct apple_dcp *dcp, enum dcp_context_id context) struct dcp_channel *ch = dcp_get_channel(dcp, context); dcp_pop_depth(&ch->depth); + if (dcp_is_ext26(dcp)) + dma_wmb(); dcp_send_message(dcp, IOMFB_ENDPOINT, dcpep_ack(context)); } @@ -1293,6 +1312,13 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, out = in + hdr->in_len; if (!handled) { + if (dcp_is_ext26(dcp)) { + dev_err(dev, "native26 unsupported callback D%03d (%u/%u); session retained\n", + tag, hdr->in_len, hdr->out_len); + WRITE_ONCE(dcp->crashed, true); + complete(&dcp->start_done); + return; + } zero_reply = dcp_external_zero_reply(dcp, tag, context, hdr->in_len, hdr->out_len); if (!zero_reply && @@ -1374,15 +1400,16 @@ static void dcpep_handle_ack(struct apple_dcp *dcp, enum dcp_context_id context) * acked on the callback context, whose receive area holds the callback * record rather than the reply, so the ack cannot be used to find it. * - * On older firmware the lengths in the record header must still be - * the ones that were sent; otherwise the record was overwritten and - * its output cannot be trusted. The tag is left out of the check: it - * has only been seen preserved on 13.5 firmware. + * Native26 preserves the command header, including its tag. Validate it + * before using the reply. Older firmware keeps its length-only check; + * other H17 profiles retain their existing admission policy. */ - if (dcp->fw_compat != DCP_FIRMWARE_H17P) { + if (dcp->fw_compat != DCP_FIRMWARE_H17P || dcp_is_ext26(dcp)) { record = out - sent->in_len - sizeof(*record); if (record->in_len != sent->in_len || - record->out_len != sent->out_len) { + record->out_len != sent->out_len || + (dcp_is_ext26(dcp) && + memcmp(record->tag, sent->tag, sizeof(record->tag)))) { dev_err(dcp->dev, "corrupt %c%c%c%c command record on context %X\n", sent->tag[3], sent->tag[2], sent->tag[1], @@ -1421,6 +1448,8 @@ static void dcpep_got_msg(struct apple_dcp *dcp, u64 message) } data = dcp->shmem + channel_offset + offset; + if (dcp_is_ext26(dcp)) + dma_rmb(); if (FIELD_GET(IOMFB_MSG_ACK, message)) dcpep_handle_ack(dcp, ctx_id); @@ -1799,7 +1828,7 @@ int dcp_get_modes(struct drm_connector *connector) dcp = platform_get_drvdata(pdev); modes = dcp_modes_snapshot(dcp, apple_connector, &nr_modes); - physical_edid = dcp->hw.iomfb_method_profile == + physical_edid = dcp_is_ext26(dcp) || dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL; /* get_modes holds mode_config.mutex; HPD paths never acquire it. */ if (physical_edid) { @@ -1822,6 +1851,8 @@ int dcp_get_modes(struct drm_connector *connector) /* Internal H17P still has no supported EDID command transport. */ if (nr_modes && !apple_connector->drm_edid && + (!dcp_is_ext26(dcp) || !dcp->ext26_edid_tried || + dcp->ext26_edid_generation != edid_generation) && (dcp->fw_compat != DCP_FIRMWARE_H17P || (dcp->dcpavservep && dcp->dcpavservep->wire_v2))) { const struct drm_edid *edid; @@ -1829,6 +1860,10 @@ int dcp_get_modes(struct drm_connector *connector) mutex_unlock(&dcp->dcpavserv.lock); if (physical_edid) mutex_unlock(&dcp->hpd_mutex); + if (dcp_is_ext26(dcp)) { + dcp->ext26_edid_tried = true; + dcp->ext26_edid_generation = edid_generation; + } edid = dcpavserv_copy_edid(dcp, edid_generation, &service_epoch); if (physical_edid) mutex_lock(&dcp->hpd_mutex); @@ -1851,6 +1886,15 @@ int dcp_get_modes(struct drm_connector *connector) /* Retirement may have queued cleanup while this request waited. */ dcp_consume_edid_invalidation(apple_connector); if (IS_ERR_OR_NULL(edid)) { + if (dcp_is_ext26(dcp) && IS_ERR(edid) && + PTR_ERR(edid) != -ENODEV && PTR_ERR(edid) != -ESTALE) { + WRITE_ONCE(dcp->ext26_blocked, true); + WRITE_ONCE(dcp->crashed, true); + apple_connector->connected = false; + nr_modes = 0; + dev_err(dcp->dev, "native26 initial EDID discovery failed: %pe; session retained\n", edid); + goto edid_done; + } /* * There is no AV service for an internal panel, nor * for an external display while its link is down; @@ -1922,6 +1966,13 @@ int dcp_get_modes(struct drm_connector *connector) } edid_done: + /* Discovery can replace the catalogue while the OOB read waits. */ + if (dcp_is_ext26(dcp)) { + if (READ_ONCE(dcp->crashed) || READ_ONCE(dcp->ext26_blocked)) + nr_modes = 0; + else + modes = dcp_modes_snapshot(dcp, apple_connector, &nr_modes); + } /* Publish after a possibly delayed raw read, under its physical generation. */ for (i = 0; i < nr_modes; ++i) { mode = drm_mode_duplicate(dev, &modes[i].mode); @@ -2028,7 +2079,7 @@ static bool dcp_mode_settable(struct apple_dcp *dcp, if (!lookup_mode(dcp, mode, &dcp_mode)) return false; - if (dcp->fw_compat == DCP_FIRMWARE_H17P && + if (dcp->fw_compat == DCP_FIRMWARE_H17P && !dcp_is_ext26(dcp) && dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL) return dcp_mode.mode.type & DRM_MODE_TYPE_PREFERRED; @@ -2065,7 +2116,7 @@ int dcp_crtc_atomic_modeset(struct drm_crtc *crtc, /* Disconnect cannot pass the admission check before these RPCs finish. */ guard(mutex)(&dcp->external_lock); - if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL && + if (dcp_is_retained_external(dcp) && (!dcp->external_running || dcp->external_error || READ_ONCE(dcp->quiescing) || READ_ONCE(dcp->crashed) || READ_ONCE(dcp->iomfb.presentation_paused))) { /* @@ -2113,7 +2164,8 @@ int dcp_crtc_atomic_modeset(struct drm_crtc *crtc, ret = iomfb_v14_7_modeset(dcp, crtc_state); break; case DCP_FIRMWARE_H17P: - ret = iomfb_modeset_h17p(dcp, crtc_state); + ret = iomfb_modeset_h17p(dcp, crtc_state, + drm_atomic_get_new_plane_state(commit, crtc->primary)); break; default: WARN_ONCE(true, "Unexpected firmware version: %u\n", @@ -2455,6 +2507,12 @@ VISIBLE_IF_KUNIT void iomfb_scanout_put_h17p(struct apple_dcp *dcp, return; if (dcp->iomfb.next_scanout == scanout) dcp->iomfb.next_scanout = NULL; + if (dcp_is_ext26(dcp)) { + /* An unsuccessful present can still have exposed its IOVAs. */ + list_add_tail(&scanout->link, &dcp->ext26_retained); + dcp->ext26_retained_bytes += ext26_scanout_bytes(scanout); + return; + } iomfb_scanout_release_h17p(scanout); } @@ -2692,7 +2750,12 @@ static void iomfb_recv_message(struct apple_dcp *dcp, u64 message) "IOMFB: init complete %#llx (version %llu)\n", message, FIELD_GET(GENMASK_ULL(63, 48), message)); - iomfb_start(dcp); + if (dcp_is_ext26(dcp)) { + dev_dbg(dcp->dev, "native26 DCPLink ready before AFK startup\n"); + complete_all(&dcp->ext26_link_ready); + } else { + iomfb_start(dcp); + } } else if (type == IOMFB_MESSAGE_TYPE_MSG) dcpep_got_msg(dcp, message); else @@ -2715,6 +2778,15 @@ void iomfb_recv_msg(struct apple_dcp *dcp, u64 message) mutex_unlock(&dcp->iomfb.lock); } +void iomfb_ext26_start(struct apple_dcp *dcp) +{ + guard(mutex)(&dcp->iomfb.lock); + WRITE_ONCE(dcp->iomfb.owner, current); + iomfb_start(dcp); + WRITE_ONCE(dcp->iomfb.owner, NULL); + iomfb_queue_advance(dcp); +} + int iomfb_start_rtkit(struct apple_dcp *dcp) { dma_addr_t shmem_iova; @@ -2737,7 +2809,8 @@ int iomfb_start_rtkit(struct apple_dcp *dcp) * display DART faults. */ if (dcp->fw_compat == DCP_FIRMWARE_H17P && - dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL) { + dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL && + !dcp_is_ext26(dcp)) { ret = apple_rtkit_start_ep(dcp->rtk, REMOTE_ALLOC_ENDPOINT); if (ret) return ret; @@ -2784,3 +2857,61 @@ void iomfb_shutdown(struct apple_dcp *dcp) #if IS_ENABLED(CONFIG_DRM_APPLE_PARSER_KUNIT_TEST) #include "iomfb-parser-test.c" #endif + +static u64 ext26_scanout_bytes(struct iomfb_scanout_h17p *scanout) +{ + u64 bytes = 0; + unsigned int i; + + for (i = 0; i < SWAP_SURFACES; i++) + if (scanout->fb[i] && scanout->fb[i]->obj[0]) + bytes += scanout->fb[i]->obj[0]->size; + return bytes; +} + +/* A release/transport boundary is not a backing-retirement witness. */ +int iomfb_ext26_retain_epoch(struct apple_dcp *dcp) +{ + struct iomfb_scanout_h17p *scanout; + u64 bytes = 0; + + guard(mutex)(&dcp->iomfb.lock); + if (!iomfb_channels_idle(dcp) || dcp->present_state_h17p.pending || + dcp->iomfb.active || dcp->iomfb.next_scanout || dcp->chunks.data || + dcp->ext26_control_pending) + return -EBUSY; + if (dcp->iomfb.scanout) + bytes += ext26_scanout_bytes(dcp->iomfb.scanout); + list_for_each_entry(scanout, &dcp->iomfb.dropped, link) + bytes += ext26_scanout_bytes(scanout); + if (!dcpext26_epoch_fits(dcp->ext26_epochs, dcp->ext26_retained_bytes, bytes)) + return -ENOSPC; + if (dcp->iomfb.scanout) { + list_add_tail(&dcp->iomfb.scanout->link, &dcp->ext26_retained); + dcp->iomfb.scanout = NULL; + } + list_splice_tail_init(&dcp->iomfb.dropped, &dcp->ext26_retained); + dcp->ext26_retained_bytes += bytes; + if (bytes) + dcp->ext26_epochs++; + dcp->ext26_blanked = false; + dcp->ext26_normal = false; + dcp->ext26_early_complete = false; + dev_dbg(dcp->dev, "native26 retained epoch %u: %llu backing bytes; remote/DMA fence unproved\n", + dcp->ext26_epochs, dcp->ext26_retained_bytes); + return 0; +} + +/* A retained old attachment may still address this GEM object. */ +bool iomfb_ext26_backing_fresh(struct apple_dcp *dcp, struct drm_framebuffer *fb) +{ + struct iomfb_scanout_h17p *scanout; + unsigned int i; + + guard(mutex)(&dcp->iomfb.lock); + list_for_each_entry(scanout, &dcp->ext26_retained, link) + for (i = 0; i < SWAP_SURFACES; i++) + if (scanout->fb[i] && scanout->fb[i]->obj[0] == fb->obj[0]) + return false; + return true; +} diff --git a/drivers/gpu/drm/apple/iomfb.h b/drivers/gpu/drm/apple/iomfb.h index 25c2e0254183f9..4b23cb0591f88c 100644 --- a/drivers/gpu/drm/apple/iomfb.h +++ b/drivers/gpu/drm/apple/iomfb.h @@ -204,8 +204,13 @@ struct dcp_method_entry { char tag[4]; /* H17G shares the H17P transport but changes selected method IDs. */ char tag_h17g[4]; + char tag_ext26[4]; }; +#define IOMFB_METHOD_EXT26(tag, native, name) [name] = { #name, \ + { tag[0], tag[1], tag[2], tag[3] }, {0}, \ + { native[0], native[1], native[2], native[3] } } + #define IOMFB_MAX_CB (1000) bool iomfb_validate_callback_h17p(int tag, u32 in_len, u32 out_len); diff --git a/drivers/gpu/drm/apple/iomfb_h17p.c b/drivers/gpu/drm/apple/iomfb_h17p.c index 4f13cee71c8720..b963642fec05a3 100644 --- a/drivers/gpu/drm/apple/iomfb_h17p.c +++ b/drivers/gpu/drm/apple/iomfb_h17p.c @@ -2,9 +2,13 @@ /* Copyright The Asahi Linux Contributors */ #include +#include +#include #include #include "dcp-link.h" +#include "dcpext26-wire.h" +#include "dcpext26-lifecycle.h" #include "iomfb_h17p.h" #include "iomfb_v12_3.h" #include "iomfb_v13_3.h" @@ -196,11 +200,12 @@ void iomfb_encode_backlight_h17p(struct dcp_present_h17p *wire, u32 nits, static const struct dcp_method_entry dcp_methods[dcpep_num_methods] = { IOMFB_METHOD("A000", dcpep_late_init_signal), - IOMFB_METHOD_H17("A025", "A029", dcpep_setup_video_limits), /* (0,0) */ + [dcpep_setup_video_limits] = { "dcpep_setup_video_limits", + {'A', '0', '2', '5'}, {'A', '0', '2', '9'}, {'A', '0', '2', '9'} }, /* (0,0) */ IOMFB_METHOD("A131", iomfbep_a131_pmu_service_matched), /* nested in D206 */ IOMFB_METHOD("A132", iomfbep_a132_backlight_service_matched), /* nested in D207 */ - IOMFB_METHOD("A385", dcpep_set_create_dfb), /* first call inside D121; (0,0) */ - IOMFB_METHOD("A386", iomfbep_a358_vi_set_temperature_hint), /* nested in D100 */ + IOMFB_METHOD_EXT26("A385", "A389", dcpep_set_create_dfb), /* first call inside D121; (0,0) */ + IOMFB_METHOD_EXT26("A386", "A390", iomfbep_a358_vi_set_temperature_hint), /* nested in D100 */ IOMFB_METHOD("A401", dcpep_start_signal), IOMFB_METHOD("A406", dcpep_swap_start), /* @@ -208,7 +213,7 @@ static const struct dcp_method_entry dcp_methods[dcpep_num_methods] = { * with it: every swap is A406 followed by A408, A407 is unused, and the * A408 response is 0x0c bytes (dcp_swap_submit_resp_h17p). */ - IOMFB_METHOD("A408", dcpep_swap_submit), + IOMFB_METHOD_EXT26("A408", "A407", dcpep_swap_submit), IOMFB_METHOD("A411", dcpep_set_display_device), IOMFB_METHOD("A412", dcpep_is_main_display), IOMFB_METHOD("A413", dcpep_set_digital_out_mode), @@ -253,6 +258,13 @@ static const struct dcp_method_entry dcp_methods[dcpep_num_methods] = { #define DCP_FW h17p #define DCP_FW_VER DCP_FW_VERSION(26, 6, 0) +static int ext26_encode(struct apple_dcp *dcp, struct dcp_swap_submit_req_h17p *request); +static int ext26_activate(struct apple_dcp *dcp); +static int ext26_open_client(struct apple_dcp *dcp); +static u8 ext26_prop_start(struct apple_dcp *dcp, u32 advertised); +static void ext26_start(struct apple_dcp *dcp); +static bool ext26_abort(struct apple_dcp *dcp, int tag, void *out, void *input); + #include "iomfb_template.c" void iomfb_apply_opaque_x_h17p(struct apple_dcp *dcp, dcp_callback_t callback, @@ -699,10 +711,465 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [599] = trampoline_nop, /* find_swap_function_gated */ }; +/* Included with the public H17P template helpers in scope. */ + +static bool ext26_callback_size(int tag, u32 input, u32 output) +{ + switch (tag) { + case 3: return input == 4 && output == 60; + case 6: return input == 84 && output == 80; + case 107: return input == 64 && !output; + case 115: return input == 16 && output == 8; + case 116: return input == 16 && output == 4; + case 134: return !input && output == 4; + case 408: return input == 8 && output == 8; + case 454: return input == 4 && output == 4; + case 567: return input == 128 && output == 4; + case 572: return input == 4 && output == 4; + case 576: return input == 4 && !output; + case 588: return !input && !output; + default: return iomfb_validate_callback_h17p(tag, input, output); + } +} + +static u8 ext26_prop_start(struct apple_dcp *dcp, u32 advertised) +{ + struct dcp_chunks *chunks = &dcp->chunks; + + if (!advertised || advertised > SZ_1M + 1) + return false; + kfree(chunks->data); + memset(chunks, 0, sizeof(*chunks)); + chunks->length = advertised - 1; + chunks->contiguous = true; + chunks->modes_generation = dcp_modes_transfer_begin(dcp); + dcp_firmware_metadata_begin(dcp); + chunks->data = kzalloc(max_t(size_t, 1, chunks->length), GFP_KERNEL); + return !!chunks->data; +} + +struct ext26_boot { + unsigned int completed; + u32 one; +}; + +static void ext26_boot_next(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct ext26_boot *boot = cookie; + const struct dcp_method_entry *method; + u32 in = 0, reply = 0; + + if (READ_ONCE(dcp->crashed) || (boot->completed && !out) || + ((boot->completed == 5 || boot->completed == 6) && + get_unaligned_le32(out) != 1)) { + WRITE_ONCE(dcp->crashed, true); + dev_err(dcp->dev, + "native26 D121 startup refused: completed=%u last=%s reply=%#x present=%u; session retained\n", + boot->completed, boot->completed == 5 ? "A000" : + boot->completed == 6 ? "A465" : "earlier call", + out && (boot->completed == 2 || boot->completed == 5 || + boot->completed == 6) ? get_unaligned_le32(out) : 0, !!out); + kfree(boot); + return; + } + if (boot->completed == 5 || boot->completed == 6) + dev_dbg(dcp->dev, "native26 D121 %s reply=%#x accepted\n", + boot->completed == 5 ? "A000" : "A465", get_unaligned_le32(out)); + switch (boot->completed++) { + case 0: + method = &dcp_methods[dcpep_set_create_dfb]; + break; + case 1: + method = &dcp_methods[dcpep_create_default_fb]; + reply = 4; + break; + case 2: + method = &dcp_methods[dcpep_setup_video_limits]; + break; + case 3: + method = &dcp_methods[dcpep_flush_supports_power]; + in = 4; + break; + case 4: + method = &dcp_methods[dcpep_late_init_signal]; + in = reply = 4; + break; + case 5: + method = &dcp_methods[dcpep_set_display_refresh_properties]; + reply = 4; + break; + default: + kfree(boot); + boot_done(dcp, NULL, NULL); + dev_dbg(dcp->dev, "native26 D121 startup complete\n"); + return; + } + dcp_push(dcp, false, method, in, reply, &boot->one, ext26_boot_next, boot); +} + +static bool ext26_boot_callback(struct apple_dcp *dcp, int tag, void *out, void *in) +{ + struct ext26_boot *boot = kzalloc_obj(*boot); + + if (!boot) { + WRITE_ONCE(dcp->crashed, true); + return false; + } + boot->one = 1; + ext26_boot_next(dcp, NULL, boot); + return false; +} + +static bool ext26_property(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + + if (tag == 107 && (!strncmp((const char *)in, "TimingElements", 64) || + !strncmp((const char *)in, "DisplayAttributes", 64))) { + dcp_modes_begin_attachment(dcp); + if (dcp->connector) { + dcp->connector->connected = false; + apple_connector_invalidate_edid(dcp->connector); + dcp_queue_hotplug(dcp->connector); + } + } + if (memcmp(in, "NormalModeActive", sizeof("NormalModeActive"))) + return true; + if (tag == 107) { + WRITE_ONCE(dcp->ext26_normal, false); + return true; + } + bool on = tag == 104 ? !!in[64] : !!get_unaligned_le32(in + 64); + + if (!on) { + WRITE_ONCE(dcp->ext26_normal, false); + return true; + } + if (READ_ONCE(dcp->ext26_power_sent) && + dcp->ext26_rx_ns > READ_ONCE(dcp->ext26_power_ns) && + dcp->ext26_generation == READ_ONCE(dcp->typec_generation)) { + WRITE_ONCE(dcp->ext26_normal, true); + complete_all(&dcp->ext26_ready); + dev_dbg(dcp->dev, "native26 NormalModeActive after power publication\n"); + } + return true; +} + +static bool ext26_bandwidth(struct apple_dcp *dcp, int tag, void *out, void *in) +{ + memset(out, 0, 60); + put_unaligned_le32(1, (u8 *)out + 56); + return true; +} + +static bool ext26_frame_sync(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + + memset(out, 0, 80); + if (!(in[80] & 1)) + memcpy(out, in, 80); + return true; +} + +static bool ext26_defaults(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + u32 count = get_unaligned_le32(in + 64); + + memset(out, 0, 36); + if (count <= 8) + memcpy(out, in + 68, count * 4); + return true; +} + +static bool ext26_clock(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + u32 index = get_unaligned_le32(in + 4); + u64 rate = 0; + + if (!memcmp(in, "VORP", 4) && index < 2) + rate = index ? 24000000 : 48000000; + put_unaligned_le64(rate, out); + return true; +} + +static bool ext26_analytics(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + + memset(out, 0, 4100); + if (memchr(in, 0, 64) && !in[4160]) + memcpy(out, in + 64, 4096); + return true; +} + +static bool ext26_tiling(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + memset(out, 0, 8); + if (((u8 *)input)[12] <= 1) + ((u8 *)out)[4] = 1; + return true; +} + +static bool ext26_release(struct apple_dcp *dcp, int tag, void *out, void *in) +{ + u32 id = get_unaligned_le32(in); + + memset(out, 0, 4); + /* Logical descriptor release never proves DMA retirement. */ + if (id && id < DCP_MAX_MAPPINGS && test_bit(id, dcp->memdesc_map) && + !test_and_set_bit(id, dcp->ext26_released)) + *(u8 *)out = 1; + return true; +} + +static bool ext26_allocate(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + u64 bytes = get_unaligned_le64(in + 4); + u32 alignment = get_unaligned_le32(in + 12); + + memset(out, 0, 28); + if (get_unaligned_le32(in) != 0x703 || !is_power_of_2(alignment) || + alignment > SZ_16K || in[16] || in[17] || in[18] || !bytes || + bytes > SZ_64M || ALIGN(bytes, SZ_16K) > SZ_256M - dcp->ext26_buffer_bytes) + return true; + trampoline_allocate_buffer_h17p(dcp, tag, out, input); + if (get_unaligned_le32((u8 *)out + 24)) + dcp->ext26_buffer_bytes += ALIGN(bytes, SZ_16K); + return true; +} + +static bool ext26_hotplug(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + u64 connected = get_unaligned_le64(in); + + memset(out, 0, 76); + if (!(in[84] & 1)) + memcpy(out, in + 8, 76); + + dcpep_cb_hotplug(dcp, &connected); + return true; +} + +static void ext26_started(struct apple_dcp *dcp, void *out, void *cookie) +{ + if (!out || READ_ONCE(dcp->crashed) || get_unaligned_le32(out) != 1) { + WRITE_ONCE(dcp->crashed, true); + complete_all(&dcp->start_done); + return; + } + dcp_ext26_open_services(dcp); + dcp->main_display = false; + dcp->active = true; + dev_dbg(dcp->dev, "native26 A401 complete; service OPEN and route admission enabled\n"); + complete_all(&dcp->start_done); +} + +static void ext26_client_reply(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_wait_cookie *wait = cookie; + + WRITE_ONCE(wait->replied, !!out); + complete(&wait->done); + kref_put(&wait->refcount, release_wait_cookie); +} + +static int ext26_open_client(struct apple_dcp *dcp) +{ + struct dcp_wait_cookie *wait; + int ret; + + if (dcp->ext26_client_open) + return 0; + wait = kzalloc_obj(*wait); + if (!wait) + return -ENOMEM; + init_completion(&wait->done); + kref_init(&wait->refcount); + kref_get(&wait->refcount); + dcp_first_client_open(dcp, false, ext26_client_reply, wait); + ret = wait_for_completion_timeout(&wait->done, msecs_to_jiffies(5000)) && + READ_ONCE(wait->replied) && !READ_ONCE(dcp->crashed) ? 0 : -ETIMEDOUT; + kref_put(&wait->refcount, release_wait_cookie); + if (!ret) { + dcp->ext26_client_open = true; + dev_dbg(dcp->dev, "native26 A457 complete before first mode\n"); + } else { + WRITE_ONCE(dcp->ext26_blocked, true); + WRITE_ONCE(dcp->crashed, true); + } + return ret; +} + +static void ext26_main_done(struct apple_dcp *dcp, void *out, void *cookie) +{ + if (!out) { + WRITE_ONCE(dcp->crashed, true); + return; + } + dcp_ack(dcp, DCP_CONTEXT_CB); +} + +static bool ext26_main_query(struct apple_dcp *dcp, int tag, void *out, void *in) +{ + dcp_is_main_display(dcp, false, ext26_main_done, NULL); + return false; +} + +static bool ext26_stride(struct apple_dcp *dcp, int tag, void *out, void *in) +{ + put_unaligned_le32(dcp->ext26_stride, out); + return true; +} + +static bool ext26_match_backlight(struct apple_dcp *dcp, int tag, void *out, void *in) +{ + iomfb_a132_backlight_service_matched(dcp, false, complete_backlight_service_matched, out); + return false; +} + +static bool ext26_map_piodma(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + const u8 *in = input; + u64 id = get_unaligned_le64(in); + + memset(out, 0, 16); + if (!id || id >= DCP_MAX_MAPPINGS || get_unaligned_le32(in + 8) != 1 || + !test_bit(id, dcp->memdesc_map) || test_bit(id, dcp->ext26_released)) + return true; + if (dcp->memdesc[id].piodma_mapped) { + put_unaligned_le32(id, out); + put_unaligned_le64(dcp->memdesc[id].dva, (u8 *)out + 4); + return true; + } + return trampoline_map_piodma_h17p(dcp, tag, out, input); +} + +static void ext26_start(struct apple_dcp *dcp) +{ + iomfb_cb_handler *handlers = kcalloc(IOMFB_MAX_CB, sizeof(*handlers), GFP_KERNEL); + + if (!handlers) { + WRITE_ONCE(dcp->crashed, true); + complete(&dcp->start_done); + return; + } + memcpy(handlers, cb_handlers, sizeof(cb_handlers)); + handlers[0] = handlers[112] = trampoline_true; + handlers[3] = ext26_bandwidth; + handlers[6] = ext26_frame_sync; + handlers[102] = handlers[104] = handlers[107] = ext26_property; + handlers[114] = ext26_analytics; + handlers[115] = ext26_tiling; + handlers[116] = handlers[134] = trampoline_zero; + handlers[588] = trampoline_nop; + handlers[121] = ext26_boot_callback; + handlers[101] = ext26_stride; + handlers[125] = ext26_defaults; + handlers[201] = ext26_map_piodma; + handlers[207] = ext26_match_backlight; + handlers[130] = handlers[411] = handlers[452] = trampoline_zero; + handlers[202] = handlers[592] = handlers[594] = trampoline_nop; + handlers[400] = handlers[401] = handlers[574] = trampoline_zero; + handlers[408] = ext26_clock; + handlers[451] = ext26_allocate; + handlers[454] = ext26_release; + handlers[575] = ext26_hotplug; + handlers[582] = trampoline_zero; + handlers[593] = ext26_abort; + handlers[599] = ext26_main_query; + dcp->ext26_handlers = handlers; + dcp->cb_handlers = handlers; + dcp_start_signal(dcp, false, ext26_started, NULL); +} + +static int ext26_encode(struct apple_dcp *dcp, struct dcp_swap_submit_req_h17p *request) +{ + struct dcpext26_primary primary, *selected = NULL; + const struct dcp_swap_submit_req_h17p *effective = request; + const struct dcp_surface *surface; + struct iomfb_scanout_h17p *scanout = dcp->iomfb.next_scanout; + struct drm_framebuffer *fb = scanout ? scanout->fb[0] : NULL; + + if (READ_ONCE(dcp->ext26_blocked) || !READ_ONCE(dcp->ext26_normal)) + return -EPIPE; + if (!(request->swap.swap_enabled & BIT(0)) && dcp->iomfb.scanout) + effective = &dcp->iomfb.scanout->request; + surface = &effective->surf[0].base; + if (!effective->surf_null[0]) { + if (!fb || !fb->obj[0]) + return -EINVAL; + primary = (struct dcpext26_primary) { + .address = effective->surf_iova[0], .backing_bytes = fb->obj[0]->size, + .width = surface->width, .height = surface->height, .pitch = surface->stride, + }; + selected = &primary; + } + return dcpext26_primary_encode(dcp->ext26_present, sizeof(dcp->ext26_present), + request->swap.swap_id, 0, selected); +} + +static void ext26_power_reply(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_wait_cookie *wait = cookie; + + wait->status = out && get_unaligned_le64(out) == 1 ? 0 : ~0U; + complete(&wait->done); + kref_put(&wait->refcount, release_wait_cookie); +} + +static bool ext26_abort(struct apple_dcp *dcp, int tag, void *out, void *input) +{ + u32 id = get_unaligned_le32(input); + + if (iomfb_ext26_control_complete(dcp, id, true)) + return true; + bool ack = trampoline_abort_swap_ap_gated(dcp, tag, out, input); + + WRITE_ONCE(dcp->ext26_blocked, true); + WRITE_ONCE(dcp->crashed, true); + return ack; +} + +static int ext26_activate(struct apple_dcp *dcp) +{ + struct dcp_set_power_state_req_h17p request = { .unklong = 1 }; + struct dcp_wait_cookie *wait; + int ret; + + wait = kzalloc_obj(*wait); + if (!wait) + return -ENOMEM; + wait->status = ~0U; + init_completion(&wait->done); + kref_init(&wait->refcount); + kref_get(&wait->refcount); + WRITE_ONCE(dcp->external_power_attempted, true); + dcp_set_power_state(dcp, false, &request, ext26_power_reply, wait); + ret = wait_for_completion_timeout(&wait->done, msecs_to_jiffies(5000)); + ret = ret && !READ_ONCE(wait->status) ? 0 : -EIO; + kref_put(&wait->refcount, release_wait_cookie); + if (!ret && !wait_for_completion_timeout(&dcp->ext26_ready, msecs_to_jiffies(5000))) + ret = -ETIMEDOUT; + if (!ret && (!READ_ONCE(dcp->ext26_normal) || READ_ONCE(dcp->crashed) || + READ_ONCE(dcp->ext26_blocked))) + ret = -EIO; + if (!ret) { + WRITE_ONCE(dcp->pipe_enabled_h17p, true); + dev_dbg(dcp->dev, "native26 mode and power ready; primary scanout admitted\n"); + } + return ret; +} + bool iomfb_check_callback_h17p(struct apple_dcp *dcp, int tag, u32 in_len, u32 out_len) { - return iomfb_validate_callback_h17p(tag, in_len, out_len); + return dcp_is_ext26(dcp) ? ext26_callback_size(tag, in_len, out_len) : + iomfb_validate_callback_h17p(tag, in_len, out_len); } static_assert(sizeof(struct dcp_set_power_state_resp) == 8); @@ -785,7 +1252,9 @@ int iomfb_external_power_h17p(struct apple_dcp *dcp, bool on) init_completion(&cookie->wait.done); kref_init(&cookie->wait.refcount); kref_get(&cookie->wait.refcount); - if (on) { + if (dcp_is_ext26(dcp)) { + dcp_set_power_state(dcp, false, &request, iomfb_external_power_complete, cookie); + } else if (on) { u32 handle = 2; iomfb_opaque_x_reset_h17p(dcp); @@ -793,7 +1262,7 @@ int iomfb_external_power_h17p(struct apple_dcp *dcp, bool on) } else { dcp_set_power_state(dcp, true, &request, iomfb_external_power_complete, cookie); } - ret = wait_for_completion_timeout(&cookie->wait.done, msecs_to_jiffies(500)); + ret = wait_for_completion_timeout(&cookie->wait.done, msecs_to_jiffies(dcp_is_ext26(dcp) ? 5000 : 500)); ret = ret ? (int)READ_ONCE(cookie->wait.status) : -ETIMEDOUT; kref_put(&cookie->wait.refcount, release_wait_cookie); return ret; @@ -801,6 +1270,10 @@ int iomfb_external_power_h17p(struct apple_dcp *dcp, bool on) void iomfb_start_h17p(struct apple_dcp *dcp) { + if (dcp_is_ext26(dcp)) { + ext26_start(dcp); + return; + } WRITE_ONCE(dcp->pipe_enabled_h17p, false); dcp->cb_handlers = cb_handlers; dcp_start_signal(dcp, false, dcp_started, NULL); @@ -839,3 +1312,107 @@ int iomfb_h17p_deliver_swap_complete(struct apple_dcp *dcp, u32 swap_id) #undef DCP_FW_VER #undef DCP_FW + +struct ext26_control { + struct dcpext26_exchange exchange; + struct completion done; + u8 wire[DCPEXT26_SUBMIT_BYTES]; +}; + +static void ext26_control_signal(struct apple_dcp *dcp, struct ext26_control *control) +{ + if (dcpext26_exchange_finished(&control->exchange)) { + if (control->exchange.failed) { + WRITE_ONCE(dcp->crashed, true); + WRITE_ONCE(dcp->ext26_blocked, true); + } + dcp->ext26_control_pending = false; + complete_all(&control->done); + } +} + +bool iomfb_ext26_control_complete(struct apple_dcp *dcp, u32 id, bool aborted) +{ + struct ext26_control *control = dcp->ext26_control; + + if (!dcp_is_ext26(dcp)) + return false; + if (id && id == dcp->ext26_control_id) + return true; + if (!control || !dcpext26_exchange_complete(&control->exchange, id)) + return false; + if (aborted) + control->exchange.failed = true; + ext26_control_signal(dcp, control); + return true; +} + +static void ext26_null_submitted(struct apple_dcp *dcp, void *reply, void *cookie) +{ + struct ext26_control *control = cookie; + + control->exchange.replied = true; + if (dcpext26_submit_reply(reply, 12)) + control->exchange.failed = true; + ext26_control_signal(dcp, control); +} + +static void ext26_null_started(struct apple_dcp *dcp, void *reply, void *cookie) +{ + struct ext26_control *control = cookie; + u32 id; + + if (dcpext26_start_reply(reply, 8, &id) || + !dcpext26_exchange_begin(&control->exchange, id) || + dcpext26_primary_encode(control->wire, sizeof(control->wire), id, 0, NULL)) { + control->exchange.failed = true; + ext26_control_signal(dcp, control); + return; + } + dcp_push(dcp, false, &dcp_methods[dcpep_swap_submit], sizeof(control->wire), 12, + control->wire, ext26_null_submitted, control); +} + +/* Called by the lifecycle worker, never from RX or the shared DRM tail. */ +int iomfb_ext26_blank(struct apple_dcp *dcp) +{ + struct ext26_control *control; + u8 start[16] = {}; + int ret; + + ret = iomfb_queue_settle(dcp, msecs_to_jiffies(5000)); + if (ret) + return ret; + control = kzalloc_obj(*control); + if (!control) + return -ENOMEM; + init_completion(&control->done); + mutex_lock(&dcp->iomfb.lock); + if (dcp->ext26_control || dcp->present_state_h17p.pending || dcp->crashed) { + mutex_unlock(&dcp->iomfb.lock); + kfree(control); + return -EBUSY; + } + dcp->ext26_control = control; + dcp->ext26_control_pending = true; + mutex_unlock(&dcp->iomfb.lock); + dcp_push(dcp, false, &dcp_methods[dcpep_swap_start], sizeof(start), 8, + start, ext26_null_started, control); + ret = wait_for_completion_timeout(&control->done, msecs_to_jiffies(5000)); + /* RX no longer owns a successful control before its storage is reclaimed. */ + if (ret) + apple_rtkit_flush_rx(dcp->rtk); + mutex_lock(&dcp->iomfb.lock); + if (!ret || control->exchange.failed || dcp->crashed) { + WRITE_ONCE(dcp->crashed, true); + ret = -EIO; + /* Possibly published control storage remains owned until reset. */ + } else { + dcp->ext26_control_id = control->exchange.id; + dcp->ext26_control = NULL; + kfree(control); + ret = 0; + } + mutex_unlock(&dcp->iomfb.lock); + return ret; +} diff --git a/drivers/gpu/drm/apple/iomfb_internal.h b/drivers/gpu/drm/apple/iomfb_internal.h index bd55bfd475f3ed..11b9b8e220da67 100644 --- a/drivers/gpu/drm/apple/iomfb_internal.h +++ b/drivers/gpu/drm/apple/iomfb_internal.h @@ -112,3 +112,8 @@ void dcp_ack(struct apple_dcp *dcp, enum dcp_context_id context); /* The user may own drm_display_mode, so we need to search for our copy */ bool lookup_mode(struct apple_dcp *dcp, const struct drm_display_mode *mode, struct dcp_display_mode *out); + +int iomfb_ext26_blank(struct apple_dcp *dcp); +bool iomfb_ext26_backing_fresh(struct apple_dcp *dcp, struct drm_framebuffer *fb); +int iomfb_ext26_retain_epoch(struct apple_dcp *dcp); +bool iomfb_ext26_control_complete(struct apple_dcp *dcp, u32 id, bool aborted); diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 78162bdb805dba..5d58df80d2ac56 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -72,6 +72,18 @@ static void dcp_swap_submit(struct apple_dcp *dcp, bool oob, { const void *data = request; + if (dcp_is_ext26(dcp)) { + if (ext26_encode(dcp, request)) { + WRITE_ONCE(dcp->crashed, true); + if (cb) + cb(dcp, NULL, cookie); + return; + } + dcp_push(dcp, oob, &dcp_methods[dcpep_swap_submit], 0x1bd8, 12, + dcp->ext26_present, cb, cookie); + return; + } + if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) { iomfb_serialize_present_h17p(&dcp->present_h17p, request); if (!iomfb_apply_backlight_h17p(dcp, request, &dcp->present_h17p)) { @@ -249,8 +261,22 @@ static u32 dcpep_cb_zero(struct apple_dcp *dcp) static void dcpep_cb_swap_complete(struct apple_dcp *dcp, struct DCP_FW_NAME(dc_swap_complete_resp) *resp) { - ktime_t now = ktime_get(); + ktime_t now = dcp_is_ext26(dcp) ? ns_to_ktime(dcp->ext26_rx_ns) : ktime_get(); bool retire = true; + if (dcp_is_ext26(dcp)) { + if (iomfb_ext26_control_complete(dcp, resp->swap_id, false)) + return; + if (resp->swap_id == dcp->last_swap_id) + return; + if (dcp->present_state_h17p.pending && !dcp->present_state_h17p.accepted && + resp->swap_id == dcp->present_state_h17p.swap_id) { + dcp->ext26_early_complete = true; + dcp->ext26_early_id = resp->swap_id; + dcp->ext26_early_ns = dcp->ext26_rx_ns; + return; + } + } + #if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G && @@ -702,15 +728,27 @@ dcpep_cb_allocate_buffer(struct apple_dcp *dcp, return resp; } - allocated.buf = dma_alloc_coherent(dcp->dev, size, &allocated.dva, - GFP_KERNEL); + if (dcp_is_ext26(dcp)) { + allocated.size = size; + allocated.buf = dma_alloc_attrs(dcp->dev, size, &allocated.dva, + GFP_KERNEL, DMA_ATTR_FORCE_CONTIGUOUS); + } else { + allocated.buf = dma_alloc_coherent(dcp->dev, size, &allocated.dva, GFP_KERNEL); + } if (!allocated.buf) return resp; ret = dma_get_sgtable(dcp->dev, &allocated.map, allocated.buf, allocated.dva, size); - if (ret) { - dma_free_coherent(dcp->dev, size, allocated.buf, allocated.dva); + if (ret || (dcp_is_ext26(dcp) && (allocated.map.orig_nents != 1 || + ((sg_phys(allocated.map.sgl) | allocated.dva) & (SZ_16K - 1))))) { + if (!ret) + sg_free_table(&allocated.map); + if (dcp_is_ext26(dcp)) + dma_free_attrs(dcp->dev, size, allocated.buf, allocated.dva, + DMA_ATTR_FORCE_CONTIGUOUS); + else + dma_free_coherent(dcp->dev, size, allocated.buf, allocated.dva); return resp; } @@ -967,6 +1005,10 @@ static void iomfbep_cb_enable_backlight_message_ap_gated(struct apple_dcp *dcp, /* Chunked data transfer for property dictionaries */ static u8 dcpep_cb_prop_start(struct apple_dcp *dcp, u32 *length) { +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + if (dcp_is_ext26(dcp)) + return ext26_prop_start(dcp, *length); +#endif if (dcp->chunks.data != NULL) { dev_warn(dcp->dev, "ignoring spurious transfer start\n"); return false; @@ -1039,8 +1081,11 @@ static u8 dcpep_cb_prop_chunk(struct apple_dcp *dcp, return false; } - if (req->offset != dcp->chunks.received) + if (req->offset != dcp->chunks.received) { dcp->chunks.contiguous = false; + if (dcp_is_ext26(dcp)) + return false; + } if (dcp->chunks.contiguous) dcp->chunks.received += req->length; memcpy(dcp->chunks.data + req->offset, req->data, req->length); @@ -1064,6 +1109,9 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, dcp_property_complete(dcp->chunks.data, dcp->chunks.received, dcp->chunks.length); + if (dcp_is_ext26(dcp) && (!dcp->chunks.contiguous || + dcp->chunks.received != dcp->chunks.length)) + return false; ret = parse(dcp->chunks.data, dcp->chunks.length, &ctx); if (ret) { @@ -1595,6 +1643,14 @@ static void dcp_prepare_clear_swap(struct apple_dcp *dcp, void *request) #endif } +static void dcp_present_rejected(struct apple_dcp *dcp) +{ + if (dcp_is_ext26(dcp)) { + WRITE_ONCE(dcp->ext26_blocked, true); + WRITE_ONCE(dcp->crashed, true); + } +} + static void dcp_swap_cleared(struct apple_dcp *dcp, void *data, void *cookie) { struct DCP_FW_NAME(dcp_swap_submit_resp) *resp = data; @@ -1608,6 +1664,8 @@ static void dcp_swap_cleared(struct apple_dcp *dcp, void *data, void *cookie) status = ~0U; if (status) { + dcp_present_rejected(dcp); + dcp->ext26_early_complete = false; /* An aborted present has already been reported. */ if (ret != -ECANCELED) dev_err(dcp->dev, "swap_clear failed! status %u\n", status); @@ -1659,7 +1717,8 @@ static void dcp_swap_clear_started(struct apple_dcp *dcp, void *data, if (info) info->swap_id = resp->swap_id; - dcp_h17p_prepare_swap(dcp); + if (!dcp_is_ext26(dcp)) + dcp_h17p_prepare_swap(dcp); dcp_swap_submit(dcp, false, &DCP_FW_UNION(dcp->swap), dcp_swap_cleared, cookie); } @@ -1709,6 +1768,11 @@ void DCP_FW_NAME(iomfb_poweron)(struct apple_dcp *dcp) int ret; u32 handle; dev_info(dcp->dev, "dcp_poweron() starting\n"); +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + if (dcp_is_ext26(dcp)) + return; /* Native26 publishes A473 only after a successful A413. */ +#endif + #if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) @@ -2185,6 +2249,8 @@ static void dcp_swapped(struct apple_dcp *dcp, void *data, void *cookie) status = ~0U; if (status) { + dcp_present_rejected(dcp); + dcp->ext26_early_complete = false; /* An aborted present has already been reported. */ if (ret != -ECANCELED) dev_err(dcp->dev, "swap failed! status %u\n", status); @@ -2195,6 +2261,17 @@ static void dcp_swapped(struct apple_dcp *dcp, void *data, void *cookie) } return; } + if (dcp_is_ext26(dcp) && dcp->ext26_early_complete && dcp->ext26_early_id == swap_id) { + struct DCP_FW_NAME(dc_swap_complete_resp) completed = { .swap_id = swap_id }; + + u64 received = dcp->ext26_rx_ns; + + dcp->ext26_early_complete = false; + dcp->ext26_rx_ns = dcp->ext26_early_ns; + dcpep_cb_swap_complete(dcp, &completed); + dcp->ext26_rx_ns = received; + return; + } dcp->swap_start = ktime_get(); dcp->swap_submit_timestamp = arch_timer_read_counter(); dcp_swap_watchdog_arm(dcp); @@ -2204,13 +2281,15 @@ static void dcp_swap_started(struct apple_dcp *dcp, void *data, void *cookie) { struct DCP_FW_NAME(dcp_swap_start_resp) *resp = data; - if (!resp || resp->ret) { + if (!resp || resp->ret || (dcp_is_ext26(dcp) && !resp->swap_id)) { + dcp_present_rejected(dcp); dev_err(dcp->dev, "swap_start was rejected\n"); if (dcp_present_retires(dcp)) dcp_drm_crtc_vblank(dcp->crtc); return; } if (!dcp_present_begin(dcp, resp->swap_id)) { + dcp_present_rejected(dcp); if (dcp_present_retires(dcp)) dcp_drm_crtc_vblank(dcp->crtc); return; @@ -2220,7 +2299,8 @@ static void dcp_swap_started(struct apple_dcp *dcp, void *data, void *cookie) dcp_arm_retained_framebuffers(dcp, resp->swap_id); trace_iomfb_swap_submit(dcp, resp->swap_id); - dcp_h17p_prepare_swap(dcp); + if (!dcp_is_ext26(dcp)) + dcp_h17p_prepare_swap(dcp); dcp_swap_submit(dcp, false, &DCP_FW_UNION(dcp->swap), dcp_swapped, NULL); } @@ -2276,7 +2356,10 @@ static void dcp_pre_swap_a428(struct apple_dcp *dcp, void *out, void *cookie) { u32 identity = 0x10000; - dcp_pipe_set_428(dcp, false, &identity, dcp_post_swap_a428, cookie); + if (dcp_is_ext26(dcp)) + do_swap(dcp, out, cookie); + else + dcp_pipe_set_428(dcp, false, &identity, dcp_post_swap_a428, cookie); } #endif @@ -2325,13 +2408,47 @@ static void dcp_set_adaptive_sync(struct apple_dcp *dcp, u32 min_vrr, } int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, - struct drm_crtc_state *crtc_state) + struct drm_crtc_state *crtc_state +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + , struct drm_plane_state *primary +#endif + ) { struct dcp_display_mode selected, *mode = &selected; struct dcp_wait_cookie *cookie; struct dcp_color_mode *cmode = NULL; int ret; +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + if (dcp_is_ext26(dcp)) { + u32 pitch = primary && primary->fb ? primary->fb->pitches[0] : dcp->ext26_stride; + + if (READ_ONCE(dcp->ext26_blocked)) + return -EIO; + if (READ_ONCE(dcp->pipe_enabled_h17p) && + (pitch != dcp->ext26_stride || + !drm_mode_equal(&dcp->ext26_mode, &crtc_state->mode))) { + /* A layout change cannot resume the old gentle-blank surface. */ + ret = dcp_ext26_layout_stop(dcp); + if (ret) + return ret; + } + dcp->ext26_stride = pitch; + if ((READ_ONCE(dcp->ext26_blanked) || READ_ONCE(dcp->ext26_normal)) && + READ_ONCE(dcp->pipe_enabled_h17p) && + dcp->ext26_generation == READ_ONCE(dcp->typec_generation) && + drm_mode_equal(&dcp->ext26_mode, &crtc_state->mode)) { + WRITE_ONCE(dcp->ext26_blanked, false); + return 0; + } + WRITE_ONCE(dcp->ext26_normal, false); + WRITE_ONCE(dcp->ext26_power_sent, false); + dcp->ext26_generation = READ_ONCE(dcp->typec_generation); + reinit_completion(&dcp->ext26_ready); + } + +#endif + if (!lookup_mode(dcp, &crtc_state->mode, mode)) { dev_err(dcp->dev, "no match for " DRM_MODE_FMT "\n", DRM_MODE_ARG(&crtc_state->mode)); @@ -2350,6 +2467,14 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, cmode = &mode->sdr; else if (mode->color_mode_id == mode->best.id) cmode = &mode->best; + if (dcp_is_ext26(dcp)) { + if (mode->sdr_rgb.score < 0 || mode->sdr_rgb.depth != 8 || + mode->sdr_rgb.format != DCP_COLOR_FORMAT_RGB || crtc_state->vrr_enabled) + return -EINVAL; + mode->color_mode_id = mode->sdr_rgb.id; + mode->vrr = false; + cmode = &mode->sdr_rgb; + } if (cmode) dev_info(dcp->dev, "set_digital_out_mode() color mode depth:%hhu format:%u " @@ -2377,8 +2502,17 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, kref_get(&cookie->refcount); dcp->swap_submit_timestamp = 0; - - if (DCP_INHERIT_BOOT_MODE && +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + if (dcp_is_ext26(dcp)) { + ret = ext26_open_client(dcp); + if (ret) { + kref_put(&cookie->refcount, release_wait_cookie); + kref_put(&cookie->refcount, release_wait_cookie); + return ret; + } + } +#endif + if (DCP_INHERIT_BOOT_MODE && !dcp_is_ext26(dcp) && dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17P_EXTERNAL) { dev_dbg(dcp->dev, "inheriting the bootloader's display mode\n"); @@ -2434,6 +2568,17 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, dev_info(dcp->dev, "set_digital_out_mode returned %#x\n", READ_ONCE(cookie->status)); } +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + if (dcp_is_ext26(dcp)) { + ret = READ_ONCE(cookie->replied) && !READ_ONCE(cookie->status) ? 0 : -EREMOTEIO; + kref_put(&cookie->refcount, release_wait_cookie); + if (!ret) + ret = ext26_activate(dcp); + if (!ret) + drm_mode_copy(&dcp->ext26_mode, &crtc_state->mode); + return ret; + } +#endif kref_put(&cookie->refcount, release_wait_cookie); dcp->vrr_enabled = mode->vrr && crtc_state->vrr_enabled; diff --git a/drivers/gpu/drm/apple/iomfb_template.h b/drivers/gpu/drm/apple/iomfb_template.h index 68021cff617618..227992942a968a 100644 --- a/drivers/gpu/drm/apple/iomfb_template.h +++ b/drivers/gpu/drm/apple/iomfb_template.h @@ -302,9 +302,14 @@ struct DCP_FW_NAME(dcp_map_reg_resp) { struct apple_dcp; +struct drm_plane_state; int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, - struct drm_crtc_state *crtc_state); + struct drm_crtc_state *crtc_state +#if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) + , struct drm_plane_state *primary +#endif + ); void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, struct drm_atomic_commit *commit); void DCP_FW_NAME(iomfb_poweron)(struct apple_dcp *dcp); void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp); diff --git a/drivers/gpu/drm/apple/j613-25g83.h b/drivers/gpu/drm/apple/j613-25g83.h index 2ac282ce95456f..ead1ef0f9b410e 100644 --- a/drivers/gpu/drm/apple/j613-25g83.h +++ b/drivers/gpu/drm/apple/j613-25g83.h @@ -43,6 +43,13 @@ static inline bool j613_25g83_mappings(const u32 *markers, size_t cells) markers[2] == 1; } +/* This profile adopts a powered RUN session through a locked firmware DART. */ +static inline bool j613_25g83_external_state(const u32 *state, size_t cells) +{ + return state && cells == 4 && state[0] == 1 && + (state[1] & BIT(4)) && (state[3] & BIT(0)); +} + /* * Local copies of the PMP table checks of * drivers/pmdomain/apple/pmp-report-validation.h, which is not in this tree diff --git a/drivers/gpu/drm/apple/j613_dcp_activate.c b/drivers/gpu/drm/apple/j613_dcp_activate.c index 6e20a5afc914c5..a5c668b5ec4b85 100644 --- a/drivers/gpu/drm/apple/j613_dcp_activate.c +++ b/drivers/gpu/drm/apple/j613_dcp_activate.c @@ -40,6 +40,13 @@ bool apple_t8122_25g83_board(void) return ok; } +/* Match the PMGR provider: automatic domains can be clock-gated while active. */ +static bool j613_power_state_active(u32 state) +{ + return FIELD_GET(GENMASK(7, 4), state) == 15 || + ((state & BIT(28)) && FIELD_GET(GENMASK(3, 0), state) == 15); +} + /* These are DT domain nodes, carrying the PMGR offsets, not copied registers. */ static int j613_power_ready(void) { @@ -82,7 +89,7 @@ static int j613_power_ready(void) return -ENOMEM; state = readl(pmgr + offset); iounmap(pmgr); - if (FIELD_GET(GENMASK(7, 4), state) != 15) + if (!j613_power_state_active(state)) return -EBUSY; } return 0; @@ -287,6 +294,196 @@ static void j613_resolve(struct device_node **nodes) "apple,t8122-asc-mailbox"); } +static struct of_changeset external_gate; +static bool external_gate_applied; + +/* + * UPDATE_PROPERTY also inserts an absent property and records its old value + * for changeset rollback. Firmware DTs may already constrain these suppliers. + */ +static int __init j613_external_update(struct device_node *np, const char *name, + const void *value, unsigned int length) +{ + struct property *prop = kzalloc_obj(*prop); + int ret; + + if (!prop) + return -ENOMEM; + prop->name = kstrdup(name, GFP_KERNEL); + prop->length = length; + if (length) + prop->value = kmemdup(value, length, GFP_KERNEL); + if (!prop->name || (length && !prop->value)) { + ret = -ENOMEM; + goto free; + } + ret = of_changeset_update_property(&external_gate, np, prop); + if (!ret) + return 0; +free: + kfree(prop->value); + kfree(prop->name); + kfree(prop); + return ret; +} + + +/* Optional external transaction; its failure must not unwind the panel. */ +static int __init j613_external_gate(struct device_node *display) +{ + struct device_node *chosen, *ext = NULL, *pio = NULL, *report = NULL; + struct device_node *nodes[9] = {}, *cpu = NULL, *routes = NULL, *route; + struct of_phandle_args args; + const char *opt, *uuid, *status, *loader_result; + const char *reason = "external node or compatible"; + struct property *prop = NULL; + const void *old; + __be32 *cells; + u32 state[4], version[3], ready; + int len, ret = -EINVAL, i, count = 0; + + chosen = of_find_node_by_path("/chosen"); + opt = chosen ? of_get_property(chosen, "asahi,j613-25g83-dcpext", &len) : NULL; + if (!opt || len != 2 || opt[0] != '1' || opt[1]) { + of_node_put(chosen); + return 0; + } + loader_result = of_get_property(chosen, "asahi,j613-25g83-dcpext-result", &len); + if (loader_result && len > 0 && loader_result[len - 1] == 0) + pr_info("J613 native26 external loader result: %s\n", loader_result); + else + pr_info("J613 native26 external loader result: absent or malformed\n"); + of_node_put(chosen); + if (!of_machine_is_compatible("apple,j613")) + return -ENODEV; + ext = of_find_node_by_path("dcpext"); + if (!ext) + ext = of_find_node_by_path("dcpext0"); +#define J613_EXT_REQUIRE(test, why) do { \ + if (!(test)) { \ + reason = why; \ + goto out; \ + } \ +} while (0) + J613_EXT_REQUIRE(ext && of_device_is_compatible(ext, "apple,t8122-dcpext"), + "external node or compatible"); + J613_EXT_REQUIRE(of_property_count_u32_elems(ext, "apple,firmware-compat") == 3 && + !of_property_read_u32_array(ext, "apple,firmware-compat", version, 3), + "missing or malformed apple,firmware-compat"); + J613_EXT_REQUIRE(!of_property_read_string(ext, "apple,firmware-uuid", &uuid), + "missing apple,firmware-uuid"); + J613_EXT_REQUIRE(j613_25g83_identity(true, 1, version, 3, uuid), "firmware identity"); + J613_EXT_REQUIRE(!of_property_read_u32(ext, "apple,j613-25g83-dcpext-handoff", &ready) && ready == 1, + "missing native handoff marker"); + J613_EXT_REQUIRE(!of_property_read_u32(ext, "apple,t8122-dcpext-firmware-ready", &ready) && ready == 1, + "missing firmware-ready marker"); + J613_EXT_REQUIRE(of_property_count_u32_elems(ext, "apple,j613-25g83-dcpext-boot-state") == 4 && + !of_property_read_u32_array(ext, "apple,j613-25g83-dcpext-boot-state", state, 4), + "missing or malformed boot-state"); + J613_EXT_REQUIRE(j613_25g83_external_state(state, 4), "boot-state not powered RUN and locked"); + J613_EXT_REQUIRE(of_property_present(ext, "memory-region"), "missing memory-region"); +#undef J613_EXT_REQUIRE + pr_info("J613 native26 external handoff: identity, markers, boot-state and memory-region accepted\n"); + reason = "PMP/DART/mailbox suppliers"; + nodes[count++] = of_node_get(ext); + nodes[count++] = j613_target(ext, "iommus", "#iommu-cells", 5, "apple,t8122-dart"); + nodes[count++] = j613_target(ext, "mboxes", "#mbox-cells", -1, "apple,t8122-asc-mailbox"); + pio = of_get_child_by_name(ext, "piodma"); + nodes[count++] = pio ? j613_target(pio, "iommus", "#iommu-cells", 4, "apple,t8122-dart") : NULL; + nodes[count++] = j613_target(ext, "apple,pmp-report", NULL, -1, + "apple,t6000-pmp-v2-report-entry"); + if (!nodes[4] || of_property_read_u32(nodes[4], "reg", &ready) || ready != 8 || + of_property_read_bool(nodes[4], "apple,no-ack") || !nodes[1] || + !nodes[3] || nodes[1] == nodes[3]) + goto out; + reason = "PMP report compatible"; + report = of_get_parent(nodes[4]); + if (!report || !of_device_is_compatible(report, "apple,j613-25g83-pmp-report")) + goto out; + reason = "DISPEXT CPU power domain"; + cpu = of_parse_phandle(ext, "power-domains", 0); + if (!cpu || of_property_read_string(cpu, "label", &status) || strcmp(status, "dispext_cpu") || + !of_property_read_bool(cpu, "apple,firmware-owned")) + goto out; + reason = "two crossbar routes"; + if (of_count_phandle_with_args(ext, "mux-controls", "#mux-control-cells") != 2) + goto out; + for (i = 0; i < 2; i++) { + if (of_parse_phandle_with_args(ext, "mux-controls", "#mux-control-cells", i, &args)) + goto out; + nodes[count++] = args.np; + if (args.args_count != 1 || args.args[0] || + !of_device_is_compatible(args.np, "apple,t6020-display-crossbar")) + goto out; + } + reason = "supplier must be disabled"; + for (i = 0; i < count; i++) { + if (!nodes[i] || of_property_read_string(nodes[i], "status", &status) || + strcmp(status, "disabled")) + goto out; + } + reason = "two Type-C route children"; + routes = of_get_child_by_name(ext, "typec-routes"); + if (of_get_child_count(routes) != 2) + goto out; + reason = "internal display IOMMU shape"; + old = of_get_property(display, "iommus", &len); + if (!old || len != 8 || !nodes[3]->phandle) + goto out; + reason = "changeset allocation"; + prop = kzalloc_obj(*prop); + cells = kmalloc(16, GFP_KERNEL); + if (!prop || !cells) { + kfree(prop); + kfree(cells); + prop = NULL; + ret = -ENOMEM; + goto out; + } + memcpy(cells, old, 8); + cells[2] = cpu_to_be32(nodes[3]->phandle); + cells[3] = 0; + prop->name = kstrdup("iommus", GFP_KERNEL); + prop->length = 16; + prop->value = cells; + if (!prop->name) { + kfree(cells); + kfree(prop); + ret = -ENOMEM; + goto out; + } + reason = "resource changeset"; + of_changeset_init(&external_gate); + ret = of_changeset_update_property(&external_gate, display, prop); + if (!ret) + ret = j613_external_update(nodes[4], "apple,always-on", NULL, 0); + for (i = 0; i < count && !ret; i++) + ret = of_changeset_update_prop_string(&external_gate, nodes[i], "status", "okay"); + for_each_child_of_node(routes, route) { + if (!ret) + ret = of_changeset_update_prop_string(&external_gate, route, "status", "okay"); + } + if (!ret) + ret = of_changeset_apply(&external_gate); + if (ret) + of_changeset_destroy(&external_gate); + else { + external_gate_applied = true; + pr_info("J613 native26 external gate accepted: reserved firmware, DISPEXT12 and both Type-C routes\n"); + } +out: + if (ret) + pr_info("J613 native26 external gate refused: %d (%s); internal display retained\n", ret, reason); + for (i = 0; i < count; i++) + of_node_put(nodes[i]); + of_node_put(cpu); + of_node_put(routes); + of_node_put(report); + of_node_put(pio); + of_node_put(ext); + return ret; +} + int __init apple_j613_25g83_coldplug(void) { struct device_node *nodes[J613_NR_NODES] = {}, *chosen; @@ -382,11 +579,17 @@ int __init apple_j613_25g83_coldplug(void) for (i = 0; i < ARRAY_SIZE(nodes) && !ret; i++) ret = of_changeset_update_prop_string(&gate, nodes[i], "status", "okay"); if (!ret) { + j613_external_gate(nodes[J613_DISPLAY]); display_clock_hz = witness[3]; ret = of_changeset_apply(&gate); } if (ret) { display_clock_hz = 0; + if (external_gate_applied) { + of_changeset_revert(&external_gate); + of_changeset_destroy(&external_gate); + external_gate_applied = false; + } of_changeset_destroy(&gate); } else { display_clock_hz = witness[3]; diff --git a/drivers/gpu/drm/apple/plane.c b/drivers/gpu/drm/apple/plane.c index cde215f62b1436..6d3dfd0f8bb9fe 100644 --- a/drivers/gpu/drm/apple/plane.c +++ b/drivers/gpu/drm/apple/plane.c @@ -5,6 +5,7 @@ #include "plane.h" +#include "dcp.h" #include "dcp-internal.h" #include "iomfb_internal.h" @@ -230,6 +231,27 @@ static int apple_plane_atomic_check(struct drm_plane *plane, if (IS_ERR(crtc_state)) return PTR_ERR(crtc_state); + { + struct platform_device *pdev = to_apple_crtc(new_plane_state->crtc)->dcp; + struct apple_dcp *target = pdev ? platform_get_drvdata(pdev) : NULL; + struct drm_framebuffer *fb = new_plane_state->fb; + + if (target && dcp_is_ext26(target) && fb && !iomfb_ext26_backing_fresh(target, fb)) + return -EINVAL; + if (target && dcp_is_ext26(target) && fb && + (plane->type != DRM_PLANE_TYPE_PRIMARY || fb->format->format != DRM_FORMAT_XRGB8888 || + fb->modifier != DRM_FORMAT_MOD_LINEAR || fb->offsets[0] || + crtc_state->vrr_enabled || new_plane_state->rotation != DRM_MODE_ROTATE_0 || + new_plane_state->src_x || new_plane_state->src_y || + new_plane_state->src_w != ((u32)crtc_state->mode.hdisplay << 16) || + new_plane_state->src_h != ((u32)crtc_state->mode.vdisplay << 16) || + new_plane_state->crtc_x || new_plane_state->crtc_y || + new_plane_state->crtc_w != crtc_state->mode.hdisplay || + new_plane_state->crtc_h != crtc_state->mode.vdisplay || + fb->width != crtc_state->mode.hdisplay || fb->height != crtc_state->mode.vdisplay)) + return -EINVAL; + } + /* * DCP limits downscaling to 2x and upscaling to 4x. Attempting to * scale outside these bounds errors out when swapping. @@ -737,15 +759,19 @@ struct drm_plane *apple_plane_init(struct drm_device *dev, bool supports_l10r, bool supports_xrgb2101010, enum drm_plane_type type, - bool interchange) + bool interchange, bool native26) { + static const u32 native_formats[] = { DRM_FORMAT_XRGB8888 }; struct apple_plane *plane; const u32 *fmts; u32 num_fmts; switch (type) { case DRM_PLANE_TYPE_PRIMARY: - if (supports_l10r && supports_xrgb2101010) { + if (native26) { + fmts = native_formats; + num_fmts = ARRAY_SIZE(native_formats); + } else if (supports_l10r && supports_xrgb2101010) { fmts = dcp_primary_formats; num_fmts = ARRAY_SIZE(dcp_primary_formats); } else if (supports_l10r) { diff --git a/drivers/gpu/drm/apple/plane.h b/drivers/gpu/drm/apple/plane.h index d7905573276f42..1236ca25892927 100644 --- a/drivers/gpu/drm/apple/plane.h +++ b/drivers/gpu/drm/apple/plane.h @@ -38,7 +38,7 @@ struct drm_plane *apple_plane_init(struct drm_device *dev, bool supports_l10r, bool supports_xrgb2101010, enum drm_plane_type type, - bool interchange); + bool interchange, bool native26); struct drm_plane *apple_plane_init_v14_7(struct drm_device *dev, unsigned long possible_crtcs, u32 iomfb_surf, diff --git a/drivers/gpu/drm/apple/systemep.c b/drivers/gpu/drm/apple/systemep.c index 6a4eefd617890f..f8ea301ab1ec05 100644 --- a/drivers/gpu/drm/apple/systemep.c +++ b/drivers/gpu/drm/apple/systemep.c @@ -138,7 +138,11 @@ int systemep_init(struct apple_dcp *dcp) init_completion(&dcp->systemep_done); dcp->systemep = afk_init(dcp, SYSTEM_ENDPOINT, systemep_ops); - afk_start(dcp->systemep); + if (IS_ERR(dcp->systemep)) + return PTR_ERR(dcp->systemep); + int ret = afk_start(dcp->systemep); + if (ret || dcp_is_ext26(dcp)) + return ret; if (!enable_verbose_logging) return 0; diff --git a/drivers/gpu/drm/asahi/g15/boot.rs b/drivers/gpu/drm/asahi/g15/boot.rs index 45e244531323d7..4e484790347329 100644 --- a/drivers/gpu/drm/asahi/g15/boot.rs +++ b/drivers/gpu/drm/asahi/g15/boot.rs @@ -317,6 +317,7 @@ static RTKIT_OPS: bindings::apple_rtkit_ops = bindings::apple_rtkit_ops { flags: 0, crashed: Some(rtkit_crashed), recv_message: Some(rtkit_recv), + recv_message_timed: None, recv_message_early: Some(rtkit_recv_early), shmem_setup: Some(rtkit_shmem_setup), shmem_destroy: Some(rtkit_shmem_destroy), diff --git a/drivers/gpu/drm/asahi/m3/m3_adt_config.rs b/drivers/gpu/drm/asahi/m3/m3_adt_config.rs index 1893c113cf2a7d..355cd44b040254 100644 --- a/drivers/gpu/drm/asahi/m3/m3_adt_config.rs +++ b/drivers/gpu/drm/asahi/m3/m3_adt_config.rs @@ -260,7 +260,14 @@ impl PstatePolicy { /// The runtime backend's InitData contents: the generated object images and the /// performance-state policy. +pub(crate) struct ExtendedInputs { + pub(crate) power: hw::PwrConfig, + pub(crate) sram_uv: KVec, + pub(crate) bases: super::m3_mmio_26::RegisterBases, +} + pub(crate) struct Contents { + pub(crate) extended: Option, /// Generated images, per owner; None for owners that stay zero. Only the three image /// owners' images are uploaded; the records' are checked. images: KVec>>, @@ -304,6 +311,10 @@ impl Contents { experiment: Option<&Experiment>, ) -> Result { let dev = pdev.as_ref(); + if firmware.profile() == super::m3_profile::Profile::Extended { + if experiment.is_some() { return Err(EINVAL); } + return Self::extended(dev, soc); + } let param = m3_params::initdata_param(); check_version(dev, firmware, experiment)?; let iomaps = match experiment { @@ -370,12 +381,54 @@ impl Contents { ); let read_only_slots = experiment.map(|e| e.read_only_slots()).unwrap_or(0); let hwdata_object = soc.hwdata_object; - Ok(Contents { images, pstates, iomaps, table, read_only_slots, hwdata_object }) + Ok(Contents { extended: None, images, pstates, iomaps, table, read_only_slots, hwdata_object }) + } + + fn extended(dev: &device::Device, soc: &Soc) -> Result { + let cfg = soc.hwcfg.ok_or(ENODEV)?; + if soc.chip_id != 0x8122 || cfg.chip_id != 0x8122 { return Err(ENODEV); } + let power = hw::PwrConfig::load(dev, cfg)?; + let tables = initdata::PStateTables::new(&power, true, None, false)?; + if tables.len != 9 || tables.max != 8 || tables.max_frequency_khz(&power) != 1_338_000 { + return Err(EINVAL); + } + let node = dev.of_node().ok_or(ENODEV)?; + let opps = node.parse_phandle(c_str!("operating-points-v2"), 0).ok_or(EINVAL)?; + let mut sram_uv = KVec::new(); + for opp in opps.children() { + let values: KVec = opp.get_property(c_str!("opp-microvolt-sram"))?; + if values.len() != 1 { return Err(EINVAL); } + sram_uv.push(values[0], GFP_KERNEL)?; + } + if sram_uv.len() != power.perf_states.len() { return Err(EINVAL); } + let block = |index: usize| -> Result { + Ok(cfg.io_mappings.get(index).and_then(|value| value.as_ref()).ok_or(EINVAL)?.base as u64) + }; + let bases = super::m3_mmio_26::RegisterBases { + gpu: soc.windows.sgx, interrupt: block(2)?, + memory: block(11)?.checked_add(0x1c4000).ok_or(EOVERFLOW)?, + telemetry_write: block(18)?, telemetry_read: block(19)?, + neural: block(25)?, metrology: block(26)?, + }; + let mut freqs = [0; 16]; + for (index, value) in freqs.iter_mut().enumerate().take(tables.len) { + *value = tables.primary_state(&power, index).freq_hz / 1_000_000; + } + // This profile enables the native firmware's thermal/PID controllers. + // The legacy host-only thermal governor must not replace those inputs. + let pstates = PstatePolicy { table_max: 8, table_max_mhz: 1338, + max: 8, safe: 8, boot: 1, max_mhz: 1338, safe_mhz: 1338, + boot_mhz: freqs[1], freqs, thermal: ThermalMode::Off }; + let mut images = KVec::new(); + for _ in 0..super::m3_init_26::COUNT { images.push(None, GFP_KERNEL)?; } + Ok(Self { extended: Some(ExtendedInputs { power, sram_uv, bases }), + images, pstates, iomaps: &[], table: (8, 1338), read_only_slots: 0, hwdata_object: None }) } /// Check the contents before they are uploaded. pub(crate) fn check_for_upload(&self) -> Result { - if self.images.len() == storage::COUNT { + let count = if self.extended.is_some() { super::m3_init_26::COUNT } else { storage::COUNT }; + if self.images.len() == count { Ok(()) } else { Err(EINVAL) diff --git a/drivers/gpu/drm/asahi/m3/m3_board.rs b/drivers/gpu/drm/asahi/m3/m3_board.rs index c44b2a42d99d55..5d4ea4277b6181 100644 --- a/drivers/gpu/drm/asahi/m3/m3_board.rs +++ b/drivers/gpu/drm/asahi/m3/m3_board.rs @@ -13,7 +13,7 @@ use crate::m3::m3_resources::{ use crate::m3::m3_soc::Soc; /// Whether the root node's compatible list contains `compatible`. -fn board_is(compatible: &[u8]) -> bool { +pub(crate) fn board_is(compatible: &[u8]) -> bool { let Some(root) = kernel::of::root() else { return false; }; diff --git a/drivers/gpu/drm/asahi/m3/m3_channel_format.rs b/drivers/gpu/drm/asahi/m3/m3_channel_format.rs new file mode 100644 index 00000000000000..45eebda112b738 --- /dev/null +++ b/drivers/gpu/drm/asahi/m3/m3_channel_format.rs @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Bounded firmware channel records. Publication and retirement belong to Config. + +use super::m3_init_layout::Error; + +pub(crate) const CONTROL_BYTES: usize = 64; +pub(crate) const RECEIVE_BYTES: usize = 72; +pub(crate) const SLOTS: u32 = 256; + +#[derive(Clone, Copy, Debug)] +pub(crate) enum Action { + Initialize, + Register { + pipe: u32, + engine: u32, + }, + Idle { + enabled: bool, + }, + Release { + group: u8, + index: u8, + weight: u8, + address: u64, + }, +} + +impl Action { + /// The first two actions precede the first generation-bearing publication. + pub(crate) fn generation(self, published: u32, consumed: u32) -> Result { + match self { + Self::Initialize | Self::Register { .. } => { + if published != 0 || consumed != 0 { + return Err(Error::Address); + } + Ok(0) + } + _ => published.max(consumed).checked_add(1).ok_or(Error::Address), + } + } + + pub(crate) fn encode(self, output: &mut [u8]) -> Result<(), Error> { + let mut words = [0u32; 3]; + let mut release = None; + match self { + Self::Initialize => words[0] = 0x16, + Self::Register { pipe, engine } => { + if pipe >= 3 || !(1..=2).contains(&engine) { + return Err(Error::Address); + } + words = [0x1b, pipe, engine]; + } + Self::Idle { enabled } => words = [0x0a, enabled.into(), 0], + Self::Release { + group, + index, + weight, + address, + } => { + if index == 255 + || weight == 255 + || address < 0xffff_fc00_0000_0000 + || address & 7 != 0 + { + return Err(Error::Address); + } + words[0] = 0x14; + words[2] = u32::from_le_bytes([group, index, weight, 4]); + release = Some(address); + } + } + let bytes = output.get_mut(..CONTROL_BYTES).ok_or(Error::Size)?; + bytes.fill(0); + for (index, word) in words.iter().enumerate() { + bytes[index * 4..index * 4 + 4].copy_from_slice(&word.to_le_bytes()); + } + if let Some(address) = release { + bytes[12..20].copy_from_slice(&address.to_le_bytes()); + } + Ok(()) + } +} + +/// An empty ring is required before publishing a synchronous control transaction. +/// Both consumers must retire it before the next control action may be published. +pub(crate) fn idle_cursor(cursors: [u32; 3]) -> Result { + if cursors.iter().any(|value| *value >= SLOTS) { + return Err(Error::Bounds); + } + if cursors[0] != cursors[2] || cursors[1] != cursors[2] { + return Err(Error::Address); + } + Ok(cursors[2]) +} + +pub(crate) fn payload_offset(thread: usize, slot: u32) -> Result { + if thread >= 9 { + return Err(Error::Bounds); + } + Ok((thread * 256 + (slot & 255) as usize) * 0xd8) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn initialization_records_have_only_their_defined_words() { + for action in [Action::Initialize, Action::Register { pipe: 2, engine: 1 }] { + let mut output = [0xa5; CONTROL_BYTES + 8]; + action.encode(&mut output).unwrap(); + assert_eq!(&output[12..CONTROL_BYTES], &[0; CONTROL_BYTES - 12]); + assert_eq!(&output[CONTROL_BYTES..], &[0xa5; 8]); + assert_eq!(action.generation(0, 0), Ok(0)); + assert!(action.generation(1, 0).is_err()); + } + } + + #[test] + fn invalid_actions_preserve_the_entire_output() { + for action in [ + Action::Register { pipe: 3, engine: 1 }, + Action::Register { pipe: 0, engine: 0 }, + Action::Release { + group: 0, + index: 255, + weight: 1, + address: 0xffff_fc21_0000_0000, + }, + Action::Release { + group: 0, + index: 0, + weight: 1, + address: 8, + }, + ] { + let mut output = [0xa5; CONTROL_BYTES]; + assert!(action.encode(&mut output).is_err()); + assert_eq!(output, [0xa5; CONTROL_BYTES]); + } + } + + #[test] + fn generations_follow_consumption_and_never_wrap() { + let action = Action::Idle { enabled: true }; + assert_eq!(action.generation(3, 5), Ok(6)); + assert_eq!(action.generation(5, 3), Ok(6)); + assert!(action.generation(u32::MAX, 0).is_err()); + assert!(action.generation(0, u32::MAX).is_err()); + let mut output = [0; CONTROL_BYTES]; + action.encode(&mut output).unwrap(); + assert_eq!(&output[..8], &[10, 0, 0, 0, 1, 0, 0, 0]); + } + + #[test] + fn both_consumers_and_all_indices_are_checked() { + assert_eq!(idle_cursor([255, 255, 255]), Ok(255)); + for cursors in [[0, 1, 1], [1, 0, 1], [256, 256, 256]] { + assert!(idle_cursor(cursors).is_err()); + } + assert_eq!(payload_offset(8, 0x101), Ok((8 * 256 + 1) * 0xd8)); + assert!(payload_offset(9, 0).is_err()); + } + + #[test] + fn release_uses_the_packed_owned_pointer() { + let action = Action::Release { + group: 1, + index: 2, + weight: 3, + address: 0xffff_fc21_0400_0000, + }; + let mut output = [0; CONTROL_BYTES]; + action.encode(&mut output).unwrap(); + assert_eq!(&output[8..12], &[1, 2, 3, 4]); + assert_eq!( + u64::from_le_bytes(output[12..20].try_into().unwrap()), + 0xffff_fc21_0400_0000 + ); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_client.rs b/drivers/gpu/drm/asahi/m3/m3_client.rs index dff1af0783965d..c1a07d61120d13 100644 --- a/drivers/gpu/drm/asahi/m3/m3_client.rs +++ b/drivers/gpu/drm/asahi/m3/m3_client.rs @@ -35,7 +35,7 @@ use crate::{ const AGX3_PARAMS_SIZE: u64 = (core::mem::offset_of!(uapi::drm_asahi_params_global, gpu_hal_generation) + core::mem::size_of::()) as u64; -const _: () = assert!(AGX3_PARAMS_SIZE == 600); +const _: () = assert!(AGX3_PARAMS_SIZE == super::m3_profile::PARAMS_REQUIRED); /// Refused clients that are logged per boot; later refusals only count. const REFUSALS_LOGGED: u32 = 16; @@ -45,10 +45,19 @@ static REFUSALS: AtomicU32 = AtomicU32::new(0); /// [`AGX3_PARAMS_SIZE`] bytes (so it knows this GPU runs AGX3 command streams), and pass /// VM_CREATE flags 0 (legacy HAL). `size` is the largest GET_PARAMS size the file asked for. pub(crate) fn admit_vm(device: &kernel::device::Device, size: u64, flags: u32) -> Result { + admit_vm_for(device, super::m3_profile::Profile::Legacy, size, flags) +} + +pub(crate) fn admit_vm_for( + device: &kernel::device::Device, + profile: super::m3_profile::Profile, + size: u64, + flags: u32, +) -> Result { { // The positive acknowledgement of the command ABI (7.1.12 `g16_profile::client_admitted`): // GET_PARAMS coverage, and VM_CREATE flags 0 on the legacy-HAL M3 runtime. - if size >= AGX3_PARAMS_SIZE && flags == 0 { + if profile.vm_acknowledged(size, flags) { return Ok(()); } let refusals = REFUSALS.fetch_add(1, Ordering::Relaxed).saturating_add(1); @@ -64,11 +73,13 @@ pub(crate) fn admit_vm(device: &kernel::device::Device, size: u64, flags: u32) - }; dev_info!( device, - "M3: refusing a VM for {}[{}]: it asked GET_PARAMS for {} of the {} parameter bytes, so it cannot know this GPU runs AGX3 command streams; M3-aware userspace is needed{}\n", + "M3: refusing a VM for {}[{}]: GET_PARAMS bytes={}/{} HAL={} flags={:#x}{}\n", name, task.group_leader().pid(), size, AGX3_PARAMS_SIZE, + profile.hal(), + flags, last ); } diff --git a/drivers/gpu/drm/asahi/m3/m3_compute.rs b/drivers/gpu/drm/asahi/m3/m3_compute.rs index 34aa0c0d2cd3fb..b6e9444ed54499 100644 --- a/drivers/gpu/drm/asahi/m3/m3_compute.rs +++ b/drivers/gpu/drm/asahi/m3/m3_compute.rs @@ -40,25 +40,28 @@ pub(crate) struct Compute { barriers: Option, /// The register lists of the SoC's GPU (`m3_soc::Soc::registers`). registers: command::RegisterSet, + profile: super::m3_profile::Profile, /// Last: drops after the command aliases. _lower_tlbi: crate::m3::m3_memory::LowerTlbi, } impl Compute { pub(crate) fn new(dev: &driver::AsahiDrmDevice, uat: &mmu::Uat, vm: &mmu::Vm, stats: Region, control: crate::m3::agx_compute::Control, - registers: command::RegisterSet) -> Result { + registers: command::RegisterSet, profile: super::m3_profile::Profile) -> Result { let lower_tlbi=crate::m3::m3_memory::LowerTlbi; let binding=uat.bind(vm)?; let mut objects=KVec::new(); let mut client_command=None; let mut command_bytes = KVec::new(); - command_bytes.resize(command::SIZE, 0u8, GFP_KERNEL)?; + command_bytes.resize(if profile == super::m3_profile::Profile::Extended { + command::EXTENDED_SIZE + } else { command::SIZE }, 0u8, GFP_KERNEL)?; let mut sequence_bytes = KVec::new(); sequence_bytes.resize(microsequence::STORAGE, 0u8, GFP_KERNEL)?; // A private compute aperture, disjoint from the retained render graph. // Address-bearing graph fields are constructed from their live owners. for index in 0..storage::COUNT { - let a=storage::batch_allocation(index).map_err(|_|EINVAL)?; + let a=storage::batch_allocation_for(profile,index).map_err(|_|EINVAL)?; let mut b=match a.mapping { Mapping::CommandAlias(va)=>Buffer::at(dev,uat.kernel_vm(),Some((uat.kernel_lower_vm(),va)),None,a.size,true)?, Mapping::Client(va)=>Buffer::at(dev,uat.kernel_vm(),Some((vm,va)),None,a.size,true)?, @@ -68,10 +71,13 @@ impl Compute { if matches!(a.mapping,Mapping::CommandAlias(_)) {client_command=Some(b.map_gpu_view(vm)?);} objects.push(b,GFP_KERNEL)?; } - Self::encode_command(&mut objects, &mut command_bytes, binding.slot(), control, 1, 0, registers)?; - Self::encode_sequence(&mut objects, &mut sequence_bytes, stats, binding.slot(), timeline::stamp(0xc1000000,1), 0)?; - UMA_POOL.initialize(&mut objects)?; - Self::initialize_state(&mut objects, binding.slot())?; + Self::encode_command(&mut objects, &mut command_bytes, binding.slot(), control, 1, 0, + registers, profile, binding.generation())?; + Self::encode_sequence(&mut objects, &mut sequence_bytes, stats, binding.slot(), timeline::stamp(0xc1000000,1), 0, profile, 0)?; + if profile == super::m3_profile::Profile::Extended { + UMA_POOL.initialize_extended(&mut objects, 1, true)?; + } else { UMA_POOL.initialize(&mut objects)?; } + Self::initialize_state(&mut objects, binding.slot(), profile)?; // Queue references are firmware VAs from live allocations. In contrast // to render's unpublished seed entries, compute dispatches entry zero // immediately, so its initial producer cursor remains one. @@ -80,8 +86,13 @@ impl Compute { job_list: address(JOB_LIST)?, gpu_scratch: address(QUEUE_SCRATCH)?, context: address(GPU_CONTEXT)?, uuid: 0xc10000 }; let command = address(storage::COMMAND)?; - let mut bytes = [0; queue::INFO_SIZE]; - info.encode(&mut bytes).map_err(|_| EINVAL)?; + let mut bytes = KVec::new(); + bytes.resize(if profile == super::m3_profile::Profile::Extended { + queue::QueueInfo::EXTENDED_SIZE + } else { queue::INFO_SIZE }, 0u8, GFP_KERNEL)?; + if profile == super::m3_profile::Profile::Extended { + info.encode_extended(address(storage::QUEUE)?, &mut bytes).map_err(|_| EINVAL)?; + } else { info.encode(&mut bytes).map_err(|_| EINVAL)?; } objects[storage::QUEUE].write(0, &bytes)?; let mut state = [0; queue::STATE_SIZE]; queue::RingState { capacity: queue::QUALIFIED_CAPACITY, done: 0, read: 0, write: 1 } @@ -107,54 +118,74 @@ impl Compute { // the invalidation. crate::mem::sync();crate::mem::tlbi_all();crate::mem::sync(); unsafe {core::arch::asm!("isb",options(nostack,preserves_flags))}; - Ok(Self {_client_command:client_command.ok_or(EINVAL)?,objects,command_bytes,sequence_bytes,_binding:binding,stats,last_progress_ns:0,sequence:1,head:1,cached_views:KVec::new(),slot:0,batch_count:1,checkpoint:(0,0,0),barriers:None,registers,_lower_tlbi:lower_tlbi}) + Ok(Self {_client_command:client_command.ok_or(EINVAL)?,objects,command_bytes,sequence_bytes,_binding:binding,stats,last_progress_ns:0,sequence:1,head:1,cached_views:KVec::new(),slot:0,batch_count:1,checkpoint:(0,0,0),barriers:None,registers,profile,_lower_tlbi:lower_tlbi}) } fn encode_command(objects: &mut [Buffer], bytes: &mut [u8], context: u32, control: crate::m3::agx_compute::Control, sequence: u64, slot: usize, - register_set: command::RegisterSet) -> Result { - let offset = |owner:usize| storage::slot_offset(owner,slot).map_err(|_|EINVAL); + register_set: command::RegisterSet, profile: super::m3_profile::Profile, generation: u8) -> Result { + let offset = |owner:usize| storage::slot_offset_for(profile,owner,slot).map_err(|_|EINVAL); let fw = |owner: usize| queue::FirmwareVa::new(objects[owner].va()+offset(owner)? as u64).map_err(|_| EINVAL); let preemption = &objects[PREEMPTION]; let value = command::Command { context, counter: timeline::previous(sequence), notifier: fw(NOTIFIER)?, - preemption: GpuRegion::new(preemption.gpu_va()?+offset(PREEMPTION)? as u64, storage::allocation(PREEMPTION).map_err(|_|EINVAL)?.size).map_err(|_| EINVAL)?, + preemption: GpuRegion::new(preemption.gpu_va()?+offset(PREEMPTION)? as u64, storage::allocation_for(profile,PREEMPTION).map_err(|_|EINVAL)?.size).map_err(|_| EINVAL)?, cdm: command::GpuVa::new(control.base).map_err(|_| EINVAL)?, cdm_last: command::GpuVa::new(control.end.checked_sub(4).ok_or(EINVAL)?).map_err(|_| EINVAL)?, gpu_alias: command::GpuVa::new(objects[storage::COMMAND].gpu_va()?+offset(storage::COMMAND)? as u64).map_err(|_| EINVAL)?, - microsequence: fw(storage::MICROSEQUENCE)?, microsequence_size: microsequence::LENGTH as u32, + microsequence: fw(storage::MICROSEQUENCE)?, microsequence_size: + if profile == super::m3_profile::Profile::Extended { microsequence::Sequence::EXTENDED_LENGTH as u32 } + else { microsequence::LENGTH as u32 }, stamp: fw(storage::STAMP)?, fw_stamp: fw(storage::FW_STAMP)?, stamp_value: timeline::stamp(0xc1000000,sequence), timestamps: [fw(TIMESTAMPS[0])?, fw(TIMESTAMPS[1])?], pool: fw(storage::POOL)?, tail_scratch: fw(TAIL_SCRATCH)?, flush_stamps: false, register_set, }; - value.encode(bytes).map_err(|_| EINVAL)?; + value.encode_with_usc(profile, generation, 0, + if profile == super::m3_profile::Profile::Extended { control.usc_base } else { 0 }, bytes).map_err(|_| EINVAL)?; objects[storage::COMMAND].write(offset(storage::COMMAND)?, bytes) } fn encode_sequence(objects: &mut [Buffer], bytes: &mut [u8], stats: Region, - context: u32, stamp_value: u32, slot: usize) -> Result { - let offset = |owner:usize| storage::slot_offset(owner,slot).map_err(|_|EINVAL); + context: u32, stamp_value: u32, slot: usize, profile: super::m3_profile::Profile, + queue_sequence: u64) -> Result { + let offset = |owner:usize| storage::slot_offset_for(profile,owner,slot).map_err(|_|EINVAL); let fw = |owner: usize| queue::FirmwareVa::new(objects[owner].va()+offset(owner)? as u64).map_err(|_| EINVAL); - let region = |owner: usize| Region::new(objects[owner].va()+offset(owner)? as u64, storage::allocation(owner).map_err(|_|EINVAL)?.size).map_err(|_| EINVAL); + let region = |owner: usize| Region::new(objects[owner].va()+offset(owner)? as u64, storage::allocation_for(profile,owner).map_err(|_|EINVAL)?.size).map_err(|_| EINVAL); let value = microsequence::Sequence { command: region(storage::COMMAND)?, stats, notifier: region(NOTIFIER)?, - queue: fw(storage::QUEUE)?, context, generation: 1, pool: fw(storage::POOL)?, + queue: fw(storage::QUEUE)?, context, + generation: if profile == super::m3_profile::Profile::Extended { 0 } else { 1 }, pool: fw(storage::POOL)?, scratch: [fw(MICROSEQUENCE_SCRATCH[0])?, fw(MICROSEQUENCE_SCRATCH[1])?, fw(MICROSEQUENCE_SCRATCH[2])?, fw(MICROSEQUENCE_SCRATCH[3])?, fw(MICROSEQUENCE_SCRATCH[4])?], fw_stamp: fw(storage::FW_STAMP)?, stamp_value, }; - value.encode(bytes).map_err(|_| EINVAL)?; + if profile == super::m3_profile::Profile::Extended { + let auxiliary = command::GpuVa::new(objects[TAIL_SCRATCH].gpu_va()? + offset(TAIL_SCRATCH)? as u64).map_err(|_| EINVAL)?; + let context_store = GpuRegion::new(objects[MICROSEQUENCE_SCRATCH[1]].gpu_va()? + + offset(MICROSEQUENCE_SCRATCH[1])? as u64, 0x40000).map_err(|_| EINVAL)?; + value.encode_extended(auxiliary, context_store, queue_sequence, bytes).map_err(|_| EINVAL)?; + } else { value.encode(bytes).map_err(|_| EINVAL)?; } objects[storage::MICROSEQUENCE].write(offset(storage::MICROSEQUENCE)?, bytes) } - fn initialize_state(objects: &mut [Buffer], context_slot: u32) -> Result { + fn initialize_state(objects: &mut [Buffer], context_slot: u32, profile: super::m3_profile::Profile) -> Result { let address = |owner: usize| queue::FirmwareVa::new(objects[owner].va()).map_err(|_| EINVAL); let notification = state::Notifier { threshold: address(storage::EVENT)?, context: context_slot }; let list_owner = address(JOB_LIST)?; let mut context = [0; state::CONTEXT_SIZE]; - state::context(&mut context).map_err(|_| EINVAL)?; - objects[GPU_CONTEXT].write(0, &context)?; - let mut notifier = [0; state::NOTIFIER_SIZE]; - notification.encode(&mut notifier).map_err(|_| EINVAL)?; + if profile == super::m3_profile::Profile::Extended { + state::context_extended(&mut context).map_err(|_| EINVAL)?; + } else { state::context(&mut context).map_err(|_| EINVAL)?; } + let context_size = if profile == super::m3_profile::Profile::Extended { + state::EXTENDED_CONTEXT_SIZE + } else { state::CONTEXT_SIZE }; + objects[GPU_CONTEXT].write(0, &context[..context_size])?; + let mut notifier = KVec::new(); + notifier.resize(if profile == super::m3_profile::Profile::Extended { + state::EXTENDED_NOTIFIER_SIZE + } else { state::NOTIFIER_SIZE }, 0u8, GFP_KERNEL)?; + if profile == super::m3_profile::Profile::Extended { + notification.encode_extended(&mut notifier).map_err(|_| EINVAL)?; + } else { notification.encode(&mut notifier).map_err(|_| EINVAL)?; } objects[NOTIFIER].write(0, ¬ifier)?; let mut list = [0; state::JOB_LIST_SIZE]; state::job_list(&mut list, list_owner).map_err(|_| EINVAL)?; @@ -182,7 +213,7 @@ impl Compute { let binding = uat.bind(vm)?; let mut views = KVec::new(); for i in 0..storage::COUNT { - if matches!(storage::allocation(i).map_err(|_|EINVAL)?.mapping,Mapping::Client(_)) { + if matches!(storage::allocation_for(self.profile,i).map_err(|_|EINVAL)?.mapping,Mapping::Client(_)) { views.push((i,self.objects[i].map_gpu_view(vm)?),GFP_KERNEL)?; } } @@ -221,18 +252,17 @@ impl Compute { if self.batch_count>=storage::SLOTS {return Err(E2BIG);} self.slot=self.batch_count; for i in storage::RESET { - let offset=storage::slot_offset(i,self.slot).map_err(|_|EINVAL)?; - let size=storage::allocation(i).map_err(|_|EINVAL)?.size; + let offset=storage::slot_offset_for(self.profile,i,self.slot).map_err(|_|EINVAL)?; + let size=storage::allocation_for(self.profile,i).map_err(|_|EINVAL)?.size; self.objects[i].fill_range(offset,size,if i==PREEMPTION {0xcc} else {0})?; } self.objects[NOTIFIER].u32(state::NOTIFIER_CONTEXT,self._binding.slot())?; self.sequence=self.sequence.checked_add(1).ok_or(EOVERFLOW)?; - Self::encode_command(&mut self.objects,&mut self.command_bytes,self._binding.slot(),control,self.sequence,self.slot,self.registers)?; + Self::encode_command(&mut self.objects,&mut self.command_bytes,self._binding.slot(),control,self.sequence,self.slot,self.registers,self.profile,self._binding.generation())?; let stamp=self.stamp(); - Self::encode_sequence(&mut self.objects,&mut self.sequence_bytes,self.stats,self._binding.slot(),stamp,self.slot)?; - self.objects[storage::EVENT].u32(0,self.sequence as u32)?; - self.objects[storage::COUNTER].u32(0,self.sequence as u32)?; - let address=self.objects[storage::COMMAND].va()+storage::slot_offset(storage::COMMAND,self.slot).map_err(|_|EINVAL)? as u64; + Self::encode_sequence(&mut self.objects,&mut self.sequence_bytes,self.stats,self._binding.slot(),stamp,self.slot,self.profile,self.sequence - 1)?; + self.write_counters()?; + let address=self.objects[storage::COMMAND].va()+storage::slot_offset_for(self.profile,storage::COMMAND,self.slot).map_err(|_|EINVAL)? as u64; self.objects[storage::RING].u64(usize::from(self.head)*8,address)?; self.head=(self.head+1)%queue::QUALIFIED_CAPACITY as u16; self.batch_count+=1; @@ -245,39 +275,55 @@ impl Compute { pub(crate) fn abort_batch(&mut self)->Result { (self.sequence,self.head,self.batch_count)=self.checkpoint; self.slot=self.batch_count.saturating_sub(1); - self.objects[storage::EVENT].u32(0,self.sequence as u32)?; - self.objects[storage::COUNTER].u32(0,self.sequence as u32)?; + self.write_counters()?; self.objects[storage::QUEUE_STATE].u32(queue::WRITE,u32::from(self.head))?; agx_memory::publish();Ok(()) } + fn write_counters(&mut self) -> Result { + for index in [storage::EVENT, storage::COUNTER] { + if self.profile == super::m3_profile::Profile::Extended { + self.objects[index].u64(0, self.sequence)?; + } else { self.objects[index].u32(0, self.sequence as u32)?; } + } + Ok(()) + } + fn offset(&self,owner:usize)->Result { - storage::slot_offset(owner,self.slot).map_err(|_|EINVAL) + storage::slot_offset_for(self.profile,owner,self.slot).map_err(|_|EINVAL) } /// Same cache-generation contract as M4: StartCompute and the persistent /// notifier must use the same tag before the firmware flushes user stamps. /// RTKit 2419: StartCompute.counter1 +0x34; JobMeta.flush_stamps +0x804. pub(crate) fn prepare_completion(&mut self,coalesce:bool)->Result { + if coalesce && self.profile == super::m3_profile::Profile::Extended { return Err(EINVAL); } let sequence=self.offset(storage::MICROSEQUENCE)?; let command=self.offset(storage::COMMAND)?; self.objects[storage::MICROSEQUENCE].u32(sequence+microsequence::GENERATION,0)?; - self.objects[storage::COMMAND].u32(command+command::FLUSH_STAMPS,u32::from(!coalesce))?; + self.objects[storage::COMMAND].u32(command+if self.profile == super::m3_profile::Profile::Extended { 0x81c } else { command::FLUSH_STAMPS },u32::from(!coalesce))?; agx_memory::publish();Ok(()) } pub(crate) fn set_user_timestamps(&mut self,addresses:[u64;2])->Result { - let offset=self.offset(storage::COMMAND)?+command::USER_TIMESTAMPS; + let offset=self.offset(storage::COMMAND)?+if self.profile == super::m3_profile::Profile::Extended { + 0x844 + } else { command::USER_TIMESTAMPS }; self.objects[storage::COMMAND].u64(offset,addresses[0])?; self.objects[storage::COMMAND].u64(offset+8,addresses[1])?; let pointer=if addresses==[0,0] {0} else {self.objects[storage::COMMAND].va()+offset as u64}; let sequence=self.offset(storage::MICROSEQUENCE)?; - for start in [microsequence::TIMESTAMP_START,microsequence::TIMESTAMP_END] { + for start in if self.profile == super::m3_profile::Profile::Extended { [0x1bc, 0x20c] } + else { [microsequence::TIMESTAMP_START,microsequence::TIMESTAMP_END] } { self.objects[storage::MICROSEQUENCE].u64(sequence+start+microsequence::USER_TIMESTAMP_POINTER,pointer)?; } agx_memory::publish();Ok(()) } pub(crate) fn set_attachments(&mut self,attachments:&crate::m3::agx_attachments::Attachments)->Result { let sequence=self.offset(storage::MICROSEQUENCE)?; - self.objects[storage::MICROSEQUENCE].write(sequence+microsequence::ATTACHMENTS,&attachments.0)?; - self.objects[storage::MICROSEQUENCE].write(sequence+microsequence::HAS_ATTACHMENTS,&[u8::from(attachments.count()!=0)])?; + self.objects[storage::MICROSEQUENCE].write(sequence+if self.profile == super::m3_profile::Profile::Extended { + microsequence::Sequence::EXTENDED_ATTACHMENTS + } else { microsequence::ATTACHMENTS },&attachments.0)?; + self.objects[storage::MICROSEQUENCE].write(sequence+if self.profile == super::m3_profile::Profile::Extended { + microsequence::Sequence::EXTENDED_HAS_ATTACHMENTS + } else { microsequence::HAS_ATTACHMENTS },&[u8::from(attachments.count()!=0)])?; agx_memory::publish();Ok(()) } fn stamp(&self) -> u32 { timeline::stamp(0xc1000000,self.sequence) } @@ -304,10 +350,12 @@ impl Compute { let barriers=self.barriers.as_mut().ok_or(EINVAL)?; let offset=self.slot*64; let mut bytes=[0;crate::m3::m3_sync_layout::BARRIER_SIZE]; - crate::m3::m3_sync_layout::Barrier { + let barrier=crate::m3::m3_sync_layout::Barrier { stamp:dependency.stamp,wait_value:dependency.value,event:dependency.event, self_value:value,uuid:0xc10000,dependency:crate::m3::m3_sync_layout::Dependency::PreviousPass, - }.encode(&mut bytes).map_err(|_|EINVAL)?; + }; + if self.profile==super::m3_profile::Profile::Extended {barrier.encode_extended(&mut bytes)} + else {barrier.encode(&mut bytes)}.map_err(|_|EINVAL)?; barriers.write(offset,&bytes)?; let address=barriers.va()+offset as u64; let (slots,next)=queue::insert_before_last(self.head,queue::QUALIFIED_CAPACITY).map_err(|_|EINVAL)?; @@ -319,26 +367,31 @@ impl Compute { agx_memory::publish();crate::mem::sync();Ok(()) } pub(crate) fn gpu_ns(&mut self)->Result { - let mut total=0; - for slot in 0..self.batch_count {total+=self.slot_gpu_ns(slot)?;} + let mut total: u64 = 0; + for slot in 0..self.batch_count { + total = total.checked_add(self.slot_gpu_ns(slot)?).ok_or(EOVERFLOW)?; + } Ok(total) } pub(crate) fn slot_gpu_ns(&mut self,slot:usize)->Result { if slot>=self.batch_count {return Err(EINVAL);} - let start=self.objects[TIMESTAMPS[0]].read_u64(storage::slot_offset(TIMESTAMPS[0],slot).map_err(|_|EINVAL)?)?; - let end=self.objects[TIMESTAMPS[1]].read_u64(storage::slot_offset(TIMESTAMPS[1],slot).map_err(|_|EINVAL)?)?; - Ok(if start!=0 && end>=start {(end-start)*1000/24} else {0}) + let start=self.objects[TIMESTAMPS[0]].read_u64(storage::slot_offset_for(self.profile,TIMESTAMPS[0],slot).map_err(|_|EINVAL)?)?; + let end=self.objects[TIMESTAMPS[1]].read_u64(storage::slot_offset_for(self.profile,TIMESTAMPS[1],slot).map_err(|_|EINVAL)?)?; + if start == 0 || end < start { return Ok(0); } + self.profile.gpu_duration_ns(end - start).ok_or(EOVERFLOW) } /// A batch span includes gaps between its compute commands, unlike gpu_ns. /// Read only after stamps, queues, events and idle state prove retirement. pub(crate) fn batch_gpu_span(&mut self)->Result<[u64;2]> { if self.batch_count == 0 {return Err(EINVAL);} - Ok([self.objects[TIMESTAMPS[0]].read_u64(storage::slot_offset(TIMESTAMPS[0],0).map_err(|_|EINVAL)?)?, - self.objects[TIMESTAMPS[1]].read_u64(storage::slot_offset(TIMESTAMPS[1],self.batch_count-1).map_err(|_|EINVAL)?)?]) + Ok([self.profile.profiler_ticks(self.objects[TIMESTAMPS[0]].read_u64(storage::slot_offset_for(self.profile,TIMESTAMPS[0],0).map_err(|_|EINVAL)?)?), + self.profile.profiler_ticks(self.objects[TIMESTAMPS[1]].read_u64(storage::slot_offset_for(self.profile,TIMESTAMPS[1],self.batch_count-1).map_err(|_|EINVAL)?)?)]) } pub(crate) fn head(&self) -> u16 { self.head } pub(crate) fn ordinal(&self)->u64 {self.sequence} - pub(crate) fn first(&self) -> bool { self.sequence == self.batch_count as u64 } + pub(crate) fn first(&self) -> bool { + self.profile == super::m3_profile::Profile::Legacy && self.sequence == self.batch_count as u64 + } pub(crate) fn progress(&mut self, dev: &driver::AsahiDrmDevice) -> Result { // Report real completions by elapsed time as well as sequence count; // batches need not land on a multiple of 128. @@ -361,10 +414,15 @@ impl Compute { pub(crate) fn queue(&self)->u64 {self.objects[storage::QUEUE].va()} pub(crate) fn complete(&mut self)->Result { - Ok(self.objects[storage::STAMP].read_u32(0)?==self.stamp() - && self.objects[storage::FW_STAMP].read_u32(0)?==self.stamp() - && self.objects[storage::QUEUE_STATE].read_u32(queue::DONE)?==u32::from(self.head) - && self.objects[storage::QUEUE_STATE].read_u32(queue::READ)?==u32::from(self.head)) + let stamped = self.objects[storage::STAMP].read_u32(0)? == self.stamp() + && self.objects[storage::FW_STAMP].read_u32(0)? == self.stamp() + && self.objects[storage::QUEUE_STATE].read_u32(queue::DONE)? == u32::from(self.head) + && self.objects[storage::QUEUE_STATE].read_u32(queue::READ)? == u32::from(self.head); + if !stamped { return Ok(false); } + if self.profile == super::m3_profile::Profile::Extended { + return Ok(self.objects[storage::POOL].read_u64(0x54)? == self.sequence); + } + Ok(true) } pub(crate) fn log(&mut self,dev:&driver::AsahiDrmDevice)->Result { for off in (0..512).step_by(128) { diff --git a/drivers/gpu/drm/asahi/m3/m3_compute_layout.rs b/drivers/gpu/drm/asahi/m3/m3_compute_layout.rs index 74bed858b71e26..ace58e38f38a6d 100644 --- a/drivers/gpu/drm/asahi/m3/m3_compute_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_compute_layout.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT use super::{m3_pool_layout::GpuRegion, m3_queue_layout::{Error, FirmwareVa}}; pub(crate) const SIZE: usize = 0x893; +pub(crate) const EXTENDED_SIZE: usize = 0x8b0; pub(crate) const REGISTERS: usize = 0x20; pub(crate) const REGISTER_COUNT: usize = 22; /// G15G takes the same list without its last two registers (0xa599, 0xd411). @@ -64,12 +65,29 @@ impl Command { /// Validate before any output mutation. Encoding operates on ordinary host /// bytes; the caller copies them into the owned unpublished command buffer. pub(crate) fn encode(&self, out: &mut [u8]) -> Result<(), Error> { - if out.len() < SIZE { return Err(Error::Size); } + self.encode_for(super::m3_profile::Profile::Legacy, 0, 0, out) + } + + /// Firmware envelopes vary independently of the GPU's CDM stream format. + pub(crate) fn encode_for(&self, profile: super::m3_profile::Profile, + asid_generation: u8, event_slot: u8, out: &mut [u8]) -> Result<(), Error> { + self.encode_with_usc(profile, asid_generation, event_slot, 0, out) + } + + pub(crate) fn encode_with_usc(&self, profile: super::m3_profile::Profile, + asid_generation: u8, event_slot: u8, usc_base: u64, out: &mut [u8]) -> Result<(), Error> { + let extended = profile == super::m3_profile::Profile::Extended; + if usc_base & 0xffff_ffff != 0 || usc_base >= 1 << 42 + || (!extended && usc_base != 0) { return Err(Error::Address); } + let size = if extended { EXTENDED_SIZE } else { SIZE }; + if out.len() < size { return Err(Error::Size); } + if extended && (self.register_set != RegisterSet::G15G || event_slot >= 4 + || (self.context != 0 && asid_generation == 0)) { return Err(Error::Address); } if self.context >= 64 || self.cdm_last.get() < self.cdm.get() { return Err(Error::Address); } if self.preemption.size() < 0x14a0 || self.microsequence_size == 0 || self.microsequence_size > 4096 { return Err(Error::Size); } - self.gpu_alias.add((SIZE - 3) as u64)?; // inclusive final aligned dword + self.gpu_alias.add(((size - 1) & !3) as u64)?; // inclusive final aligned dword let registers = self.gpu_alias.add(REGISTERS as u64)?; let preemption = self.preemption.base(); let values = [ @@ -82,45 +100,157 @@ impl Command { (0x1a0b9, 0), (0x1a0c1, 0), (0x101d1, 0), (0xd479, 0), (0x1a0e9, 8), (0x107a1, 0xff0000), (0xa599, 0x13200400020), (0xd411, 0x200000001), ]; - let bytes = &mut out[..SIZE];bytes.fill(0); + let bytes = &mut out[..size];bytes.fill(0); word(bytes, 0, 3); // compute opcode - pointer(bytes, COUNTER, u64::from(self.counter)); - word(bytes, CONTEXT, self.context); - pointer(bytes, 0x14, self.notifier.get()); - let count = match self.register_set { + if extended { + word(bytes, 0xc, self.context); + pointer(bytes, 0x10, self.notifier.get()); + } else { + pointer(bytes, COUNTER, u64::from(self.counter)); + word(bytes, CONTEXT, self.context); + pointer(bytes, 0x14, self.notifier.get()); + } + let legacy_count = match self.register_set { RegisterSet::G15S => REGISTER_COUNT, RegisterSet::G15G => G15G_REGISTER_COUNT, }; - for (n, (register, value)) in values.into_iter().take(count).enumerate() { - word(bytes, REGISTERS+n*REGISTER_STRIDE, register); - pointer(bytes, REGISTERS+n*REGISTER_STRIDE+4, value); + let mut count = 0; + let mut emit = |register, value| { + word(bytes, REGISTERS + count * REGISTER_STRIDE, register); + pointer(bytes, REGISTERS + count * REGISTER_STRIDE + 4, value); + count += 1; + }; + let limit = if extended { REGISTER_COUNT } else { legacy_count }; + for (register, value) in values.into_iter().take(limit) { + if extended && register == 0x12091 { continue; } + emit(register, value); + } + if extended { + emit(0x1a540, 1); + emit(0x014a9, 1); + emit(0x0a351, 1); + if usc_base != 0 { emit(0x10071, usc_base); } } pointer(bytes, 0x720, registers.get()); bytes[0x728..0x72a].copy_from_slice(&(count as u16).to_le_bytes()); bytes[0x72a..0x72c].copy_from_slice(&((count*REGISTER_STRIDE) as u16).to_le_bytes()); pointer(bytes, 0x760, self.microsequence.get()); word(bytes, 0x768, self.microsequence_size); - // ComputeInfo2 mirrors the preemption buffer, inclusive CDM end and - // qualified control word from the register list. - pointer(bytes, 0x76c+0x28, preemption); - pointer(bytes, 0x76c+0x30, self.cdm_last.get()); - pointer(bytes, 0x76c+0x58, QUALIFIED_COMPUTE_CONTROL); - word(bytes, 0x7cc+0x14, u32::MAX); // EncoderParams.unk_mask - pointer(bytes, META, self.stamp.get()); - pointer(bytes, META+8, self.fw_stamp.get()); - word(bytes, STAMP_VALUE, self.stamp_value); - word(bytes, META+0x14, 2); // qualified compute event slot - word(bytes, FLUSH_STAMPS, u32::from(self.flush_stamps)); - word(bytes, META+0x20, 0xc10000); // producer UUID - word(bytes, QUEUE_COUNT, self.counter); - pointer(bytes, TAIL+8, self.timestamps[0].get()); - pointer(bytes, TAIL+16, self.timestamps[1].get()); - pointer(bytes, TAIL+0x2e, self.pool.get()); - bytes[TAIL+0x36] = 1; // SharedCommandTail.unk_846 - pointer(bytes, TAIL+0x37, 0xad0000); - pointer(bytes, TAIL+0x3f, 0x1600000); - pointer(bytes, TAIL+0x47, self.tail_scratch.get()); - bytes[TAIL+0x4f] = 1; // SharedCommandTail.unk_85f + let info = if extended { 0x770 } else { 0x76c }; + pointer(bytes, info + 0x28, preemption); + pointer(bytes, info + 0x30, self.cdm_last.get()); + pointer(bytes, info + 0x58, QUALIFIED_COMPUTE_CONTROL); + word(bytes, if extended { 0x7e8 } else { 0x7e0 }, u32::MAX); + let meta = if extended { 0x800 } else { META }; + pointer(bytes, meta, self.stamp.get()); + pointer(bytes, meta + 8, self.fw_stamp.get()); + word(bytes, meta + 0x10, self.stamp_value); + word(bytes, meta + 0x14, 2); + if extended { word(bytes, meta + 0x18, event_slot.into()); } + word(bytes, meta + 0x1c, u32::from(self.flush_stamps)); + word(bytes, meta + 0x20, 0xc10000); + word(bytes, meta + 0x24, self.counter); + let tail = if extended { 0x82c } else { TAIL }; + pointer(bytes, tail + 8, self.timestamps[0].get()); + pointer(bytes, tail + 16, self.timestamps[1].get()); + pointer(bytes, tail + if extended { 0x3e } else { 0x2e }, self.pool.get()); + bytes[tail + if extended { 0x46 } else { 0x36 }] = 1; + pointer(bytes, tail + if extended { 0x47 } else { 0x37 }, + if extended { 0x1588000 } else { 0xad0000 }); + pointer(bytes, tail + if extended { 0x4f } else { 0x3f }, + if extended { 0xef8000 } else { 0x1600000 }); + pointer(bytes, tail + if extended { 0x57 } else { 0x47 }, self.tail_scratch.get()); + if extended { bytes[0x88b] = asid_generation; } + else { bytes[TAIL + 0x4f] = 1; } Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use super::super::m3_profile::Profile; + + fn fixture() -> Command { + let firmware = |n: u64| FirmwareVa::new(0xffff_fc21_8000_0000 + n * 0x4000).unwrap(); + Command { + context: 3, counter: 7, notifier: firmware(0), + preemption: GpuRegion::new(0x5000_0000, 0x4000).unwrap(), + cdm: GpuVa::new(0x6000_0000).unwrap(), + cdm_last: GpuVa::new(0x6000_0ffc).unwrap(), + gpu_alias: GpuVa::new(0x7000_0000).unwrap(), + microsequence: firmware(1), microsequence_size: 0x300, + stamp: firmware(2), fw_stamp: firmware(3), stamp_value: 0x700, + timestamps: [firmware(4), firmware(5)], pool: firmware(6), + tail_scratch: firmware(7), flush_stamps: true, + register_set: RegisterSet::G15G, + } + } + fn read32(b: &[u8], o: usize) -> u32 { + u32::from_le_bytes(b[o..o + 4].try_into().unwrap()) + } + fn read64(b: &[u8], o: usize) -> u64 { + u64::from_le_bytes(b[o..o + 8].try_into().unwrap()) + } + + #[test] + fn extended_envelope_preserves_stream_and_native_visibility_boundary() { + let c = fixture(); + let mut b = [0xa5; EXTENDED_SIZE + 16]; + c.encode_for(Profile::Extended, 9, 2, &mut b).unwrap(); + assert_eq!(read32(&b, 0xc), c.context); + assert_eq!(read64(&b, 0x10), c.notifier.get()); + assert_eq!(read32(&b, 0x728) & 0xffff, 24); + let count = (read32(&b, 0x728) & 0xffff) as usize; + let cdm = (0..count).find(|n| read32(&b, 0x20 + n * 12) == 0x1a420).unwrap(); + assert_eq!(read64(&b, 0x24 + cdm * 12), c.cdm.get()); + assert_eq!(read64(&b, 0x7a0), c.cdm_last.get()); + assert_eq!(read64(&b, 0x800), c.stamp.get()); + assert_eq!(read64(&b, 0x808), c.fw_stamp.get()); + assert_eq!(read32(&b, 0x818), 2); + assert_eq!(read32(&b, 0x81c), 1); + assert_eq!(read64(&b, 0x834), c.timestamps[0].get()); + assert_eq!(read64(&b, 0x86a), c.pool.get()); + assert_eq!(read64(&b, 0x883), c.tail_scratch.get()); + assert_eq!(b[0x88b], 9); + assert_eq!(&b[EXTENDED_SIZE..], &[0xa5; 16]); + } + + #[test] + fn extended_validation_never_mutates_a_rejected_envelope() { + for (generation, slot) in [(0, 0), (1, 4), (1, 255)] { + let mut b = [0xa5; EXTENDED_SIZE]; + assert!(fixture().encode_for(Profile::Extended, generation, slot, &mut b).is_err()); + assert_eq!(b, [0xa5; EXTENDED_SIZE]); + } + let mut b = [0xa5; EXTENDED_SIZE - 1]; + assert!(fixture().encode_for(Profile::Extended, 1, 0, &mut b).is_err()); + assert_eq!(b, [0xa5; EXTENDED_SIZE - 1]); + } + + #[test] + fn legacy_wrapper_keeps_legacy_geometry_and_register_order() { + let c = fixture(); + let mut b = [0xa5; EXTENDED_SIZE]; + c.encode(&mut b).unwrap(); + assert_eq!(read32(&b, CONTEXT), c.context); + assert_eq!(read64(&b, 0x14), c.notifier.get()); + assert_eq!(read32(&b, 0x728) & 0xffff, 20); + assert_eq!(read32(&b, REGISTERS + 8 * REGISTER_STRIDE), 0x12091); + assert_eq!(read64(&b, META), c.stamp.get()); + assert_eq!(&b[SIZE..], &[0xa5; EXTENDED_SIZE - SIZE]); + } + #[test] + fn optional_usc_window_is_a_dispatch_register_without_stream_rewriting() { + let c = fixture(); + let mut output = [0xa5; EXTENDED_SIZE]; + c.encode_with_usc(Profile::Extended, 1, 0, 1 << 32, &mut output).unwrap(); + assert_eq!(read32(&output, 0x728) & 0xffff, 25); + assert_eq!(read32(&output, REGISTERS + 24 * REGISTER_STRIDE), 0x10071); + assert_eq!(read64(&output, REGISTERS + 24 * REGISTER_STRIDE + 4), 1 << 32); + output.fill(0xa5); + assert!(c.encode_with_usc(Profile::Extended, 1, 0, 1, &mut output).is_err()); + assert_eq!(output, [0xa5; EXTENDED_SIZE]); + } + +} diff --git a/drivers/gpu/drm/asahi/m3/m3_compute_sequence.rs b/drivers/gpu/drm/asahi/m3/m3_compute_sequence.rs index a10c3040d570f0..bd78c03823aa07 100644 --- a/drivers/gpu/drm/asahi/m3/m3_compute_sequence.rs +++ b/drivers/gpu/drm/asahi/m3/m3_compute_sequence.rs @@ -82,3 +82,113 @@ impl Sequence { Ok(()) } } + +impl Sequence { + pub(crate) const EXTENDED_LENGTH: usize = 0x300; + pub(crate) const EXTENDED_ATTACHMENTS: usize = 0x58; + pub(crate) const EXTENDED_HAS_ATTACHMENTS: usize = 0x2c0; + + /// The extended firmware operates on a native G15 CDM stream. Its firmware + /// microsequence has wider timestamp operations and different engine state. + pub(crate) fn encode_extended(&self, auxiliary: command::GpuVa, + context_store: super::m3_pool_layout::GpuRegion, queue_sequence: u64, + output: &mut [u8]) -> Result<(), Error> { + if self.context >= 64 || self.generation != 0 { return Err(Error::Address); } + if context_store.size() < 0x20000 { return Err(Error::Size); } + let registers = self.command.at(command::REGISTERS, 24 * command::REGISTER_STRIDE)?; + let engine = self.command.at(0x76c, 0x60)?; + let clock = self.command.at(0x82c, 8)?; + let pairs = self.command.at(0x834, 16)?; + let end_pair = self.command.at(0x83c, 8)?; + let pool_pointer = self.command.at(0x86a, 8)?; + let status = self.command.at(0x8ac, 4)?; + let time_aux = self.command.at(0x89c, 8)?; + let elapsed = self.command.at(0x7dc, 8)?; + let tail = self.command.at(0x88c, 8)?; + let activity = self.stats.at(0, 8)?; + let bytes = output.get_mut(..STORAGE).ok_or(Error::Size)?; + bytes.fill(0); + word(bytes, 0, 0x0b); + pointer(bytes, 0x14, registers); + pointer(bytes, 0x1c, activity); + pointer(bytes, 0x24, self.queue.get()); + word(bytes, 0x2c, self.context); + word(bytes, 0x30, 1); + pointer(bytes, 0x38, queue_sequence); + pointer(bytes, 0x44, engine); + word(bytes, 0x50, 1); + pointer(bytes, 0x160, self.pool.get()); + pointer(bytes, 0x178, auxiliary.get()); + for (offset, delta) in [0, 0xf400, 0x1e800, 0x1f000].into_iter().enumerate() { + pointer(bytes, 0x188 + offset * 8, context_store.base() + delta); + } + pointer(bytes, 0x1a8, status); + word(bytes, 0x1b8, 2); + // Timestamp records are 76 bytes; WFI sits between the two records. + for (offset, opcode, selected) in [(0x1bc, 0x8000_0003, pairs), (0x20c, 3, end_pair)] { + word(bytes, offset, opcode); + pointer(bytes, offset + 4, clock); + pointer(bytes, offset + 0xc, pairs); + pointer(bytes, offset + 0x14, selected); + pointer(bytes, offset + 0x1c, self.queue.get()); + pointer(bytes, offset + 0x2c, time_aux); + pointer(bytes, offset + 0x34, elapsed); + pointer(bytes, offset + 0x44, 0xc10000); + } + word(bytes, 0x208, 1); + let finish = 0x258; + word(bytes, finish, 0x0c); + pointer(bytes, finish + 4, activity); + pointer(bytes, finish + 0xc, self.queue.get()); + word(bytes, finish + 0x14, self.context); + pointer(bytes, finish + 0x18, engine); + pointer(bytes, finish + 0x24, 0xc10000); + pointer(bytes, finish + 0x2c, self.fw_stamp.get()); + word(bytes, finish + 0x34, self.stamp_value); + pointer(bytes, finish + 0x5c, pool_pointer); + word(bytes, finish + 0x64, (-(finish as i32)) as u32); + pointer(bytes, finish + 0x69, status); + pointer(bytes, finish + 0x71, tail); + word(bytes, 0x2d4, 0x4000_0002); + Ok(()) + } +} + +#[cfg(test)] +mod extended_tests { + use super::*; + fn fixture() -> Sequence { + let fw = |i: u64| FirmwareVa::new(0xffff_fc21_a000_0000 + i * 0x4000).unwrap(); + Sequence { command: Region::new(fw(0).get(), command::EXTENDED_SIZE).unwrap(), + stats: Region::new(fw(1).get(), 0xe10).unwrap(), + notifier: Region::new(fw(2).get(), 0x200).unwrap(), queue: fw(3), + context: 3, generation: 0, pool: fw(4), scratch: [fw(5); 5], + fw_stamp: fw(6), stamp_value: 0x700 } + } + fn ptr(bytes: &[u8], at: usize) -> u64 { u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap()) } + #[test] fn extended_timestamps_and_retirement_refer_to_their_live_owners() { + let s = fixture(); + let mut output = [0xa5; STORAGE + 8]; + s.encode_extended(command::GpuVa::new(0x5000_0000).unwrap(), + super::super::m3_pool_layout::GpuRegion::new(0x6000_0000, 0x20000).unwrap(), 7, &mut output).unwrap(); + assert_eq!(ptr(&output, 0x1c), s.stats.at(0, 8).unwrap()); + assert_eq!(ptr(&output, 0x38), 7); + assert_eq!(ptr(&output, 0x188), 0x6000_0000); + assert_eq!(ptr(&output, 0x1bc + 0x14), s.command.at(0x834, 8).unwrap()); + assert_eq!(ptr(&output, 0x20c + 0x14), s.command.at(0x83c, 8).unwrap()); + assert_eq!(ptr(&output, 0x258 + 0x2c), s.fw_stamp.get()); + assert_eq!(ptr(&output, 0x258 + 0x5c), s.command.at(0x86a, 8).unwrap()); + assert_eq!(&output[STORAGE..], &[0xa5; 8]); + } + #[test] fn extended_sequence_rejects_legacy_command_extents_and_event_generations() { + for generation in [0, 1] { + let mut s = fixture(); + s.generation = generation; + if generation == 0 { s.command = Region::new(s.command.at(0, 1).unwrap(), command::SIZE).unwrap(); } + let mut output = [0xa5; STORAGE]; + assert!(s.encode_extended(command::GpuVa::new(0x5000_0000).unwrap(), + super::super::m3_pool_layout::GpuRegion::new(0x6000_0000, 0x20000).unwrap(), 0, &mut output).is_err()); + assert_eq!(output, [0xa5; STORAGE]); + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_compute_storage.rs b/drivers/gpu/drm/asahi/m3/m3_compute_storage.rs index 6bd3aecda387d3..a897a45f0922de 100644 --- a/drivers/gpu/drm/asahi/m3/m3_compute_storage.rs +++ b/drivers/gpu/drm/asahi/m3/m3_compute_storage.rs @@ -87,3 +87,69 @@ pub(crate) fn allocation(index:usize)->Result { pub(crate) fn coalesce_stamp_flush(wide:bool,timestamps:[u64;2],last:bool)->bool { wide && timestamps==[0,0] && !last } + +pub(crate) fn allocation_for(profile: super::m3_profile::Profile, index: usize) + -> Result { + let mut owner = allocation(index)?; + if profile == super::m3_profile::Profile::Legacy { return Ok(owner); } + match index { + // The initial allocator has eight 4-MiB payload owners. Retain the + // historical indices without allocating unused 4-MiB payloads. + 9..=22 => { owner.size = 0x4000; owner.mapping = Mapping::FirmwareOnly; } + POOL_PAGES => owner.size = 0x20000, + POOL_TABLE => { owner.size = 0x4000; owner.mapping = Mapping::Client(POOL_TABLE_VA & !0x3fff); } + POOL => owner.size = 0xa0, + GPU_CONTEXT => owner.size = 0x38, + NOTIFIER => owner.size = 0x200, + EVENT => owner.size = 8, + QUEUE => owner.size = 0x24c0, + PREEMPTION => owner.size = 0x4000, + COMMAND => { owner.size = 0x8b0; owner.mapping = Mapping::CommandAlias(0x73_0000_0000); } + TAIL_SCRATCH => { owner.size = 0x4000; owner.mapping = Mapping::Client(0x72_0100_0000); } + 46 => { owner.size = 0x40000; owner.mapping = Mapping::Client(0x72_0000_0000); } + 45 | 47..=49 => { owner.size = 0x4000; owner.mapping = Mapping::FirmwareOnly; } + _ => {} + } + Ok(owner) +} + +pub(crate) fn slot_offset_for(profile: super::m3_profile::Profile, index: usize, slot: usize) + -> Result { + if slot >= SLOTS { return Err(InvalidOwner); } + let size = allocation_for(profile, index)?.size; + Ok(if RESET.contains(&index) { ((size + 0x3fff) & !0x3fff) * slot } else { 0 }) +} + +pub(crate) fn batch_allocation_for(profile: super::m3_profile::Profile, index: usize) + -> Result { + let mut owner = allocation_for(profile, index)?; + owner.size += slot_offset_for(profile, index, SLOTS - 1)?; + Ok(owner) +} + +#[cfg(test)] +mod extended_tests { + use super::*; + use super::super::m3_profile::Profile; + #[test] fn extended_commands_do_not_straddle_another_slots_page() { + let command = allocation_for(Profile::Extended, COMMAND).unwrap(); + let Mapping::CommandAlias(base) = command.mapping else { panic!("missing command alias") }; + assert_eq!(base & 0x3fff, 0); + for slot in 0..SLOTS - 1 { + let start = slot_offset_for(Profile::Extended, COMMAND, slot).unwrap(); + let next = slot_offset_for(Profile::Extended, COMMAND, slot + 1).unwrap(); + assert!(start + command.size <= next); + } + assert_eq!(batch_allocation_for(Profile::Extended, QUEUE).unwrap().size, 0x24c0); + assert!(slot_offset_for(Profile::Extended, COMMAND, SLOTS).is_err()); + } + #[test] fn legacy_plan_is_byte_for_byte_the_same_for_every_owner_and_slot() { + for index in 0..COUNT { + assert_eq!(allocation_for(Profile::Legacy, index), allocation(index)); + for slot in 0..SLOTS { + assert_eq!(slot_offset_for(Profile::Legacy, index, slot), slot_offset(index, slot)); + } + assert_eq!(batch_allocation_for(Profile::Legacy, index), batch_allocation(index)); + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_config.rs b/drivers/gpu/drm/asahi/m3/m3_config.rs index c4a44c4b4f3b0e..02b847e210dc9b 100644 --- a/drivers/gpu/drm/asahi/m3/m3_config.rs +++ b/drivers/gpu/drm/asahi/m3/m3_config.rs @@ -49,6 +49,9 @@ impl Records { pub(crate) struct Config { objects: KVec, + profile: super::m3_profile::Profile, + control_generation: u32, + io_descriptors: Option>, _iomaps: KVec, pub(crate) completed_events: u64, pstates: crate::m3::m3_adt_config::PstateWatch, @@ -65,6 +68,17 @@ impl Config { pub(crate) fn new(dev: &driver::AsahiDrmDevice, uat: &mmu::Uat, firmware: &crate::m3::m3_firmware::Firmware, contents: &crate::m3::m3_adt_config::Contents) -> Result { contents.check_for_upload()?; + if let Some(inputs) = &contents.extended { + if firmware.profile() != super::m3_profile::Profile::Extended { return Err(EINVAL); } + let owners = ExtendedStorage::new(dev, uat, &inputs.power, &inputs.sram_uv, inputs.bases)?; + let thermal = crate::m3::m3_thermal::Governor::new(dev.as_ref().as_ref(), &contents.pstates)?; + return Ok(Self { objects: owners.objects, _iomaps: owners.iomaps, + profile: firmware.profile(), control_generation: 0, completed_events: 0, + io_descriptors: Some(owners.descriptors), + pstates: crate::m3::m3_adt_config::PstateWatch::new(contents.pstates), thermal, + request_top: 8, request_value: 0, request_checked: false, request_check_at: None, + request_writes: 0, request_observed: 0 }); + } let mut objects = KVec::new(); let mut iomaps = KVec::new(); for index in 0..storage::COUNT { @@ -140,7 +154,8 @@ impl Config { crate::m3::m3_adt_config::check_upload(dev.as_ref().as_ref(), firmware, contents.iomaps, &mut objects)?; agx_memory::publish(); let thermal = crate::m3::m3_thermal::Governor::new(dev.as_ref().as_ref(), &contents.pstates)?; - Ok(Self { objects, _iomaps: iomaps, completed_events: 0, + Ok(Self { objects, profile: firmware.profile(), control_generation: 0, io_descriptors: None, + _iomaps: iomaps, completed_events: 0, pstates: crate::m3::m3_adt_config::PstateWatch::new(contents.pstates), thermal, request_top:contents.table.0,request_value:0,request_checked:false, request_check_at:None,request_writes:0,request_observed:0 }) @@ -153,6 +168,25 @@ impl Config { objects[RUNTIME_POINTERS].write(0, &records.runtime)?; objects[CONTROL_REGION].write(0, &records.control) } + pub(crate) fn publish_io_table(&self, + pdev: &kernel::platform::Device>, + firmware: &crate::m3::m3_firmware::Firmware) -> Result { + if let Some(descriptors) = &self.io_descriptors { + firmware.publish_io_table(pdev, descriptors)?; + } + Ok(()) + } + + pub(crate) fn global_table_views(&mut self, vm: &mmu::Vm) -> Result> { + let mut mappings = KVec::new(); + if self.profile == super::m3_profile::Profile::Extended { + for index in [super::m3_init_26::GLOBAL_POOL, super::m3_init_26::GLOBAL_PARAMETERS] { + mappings.push(self.objects[index].map_gpu_view(vm)?, GFP_KERNEL)?; + } + } + Ok(mappings) + } + pub(crate) fn root(&self) -> u64 { self.objects[storage::ROOT].va() } pub(crate) fn log_globals_debug(&mut self,dev:&driver::AsahiDrmDevice)->Result { let value=self.objects[GLOBALS].read_u32(crate::m3::m3_globals_debug::OFFSET)?; @@ -161,6 +195,8 @@ impl Config { Ok(()) } pub(crate) fn performance_request(&mut self,chip:u32,busy:bool)->Result { + // Native controller inputs supersede the legacy fixed-request interface. + if self.profile == super::m3_profile::Profile::Extended { return Ok(()); } let Some(value)=crate::m3::m3_consistent_perf::request(chip, crate::m3::m3_params::consistent_perf(),busy,self.request_top) else {return Ok(());}; if value==0 && self.request_value==0 {return Ok(());} @@ -192,6 +228,14 @@ impl Config { /// state the firmware reports against the ceiling and the runtime cap. Called after boot and /// after every job, by the job thread. pub(crate) fn check_pstate(&mut self, dev: &driver::AsahiDrmDevice, device: &crate::m3::m3_device::Device, what: &str) -> Result { + if self.profile == super::m3_profile::Profile::Extended { + let state = device.pstate_register()? & 15; + if state > self.request_top { + dev_err!(dev.as_ref(), "M3: native performance state {} exceeds {} ({})\n", state, self.request_top, what); + return Err(ERANGE); + } + return Ok(()); + } let now = kernel::time::Instant::::now(); if let Some(cap) = self.thermal.update(dev.as_ref().as_ref(), now) { crate::m3::m3_adt_config::publish_runtime_cap(&mut self.objects[GLOBALS], cap)?; @@ -202,14 +246,38 @@ impl Config { self.thermal.busy(now); Ok(()) } + pub(crate) fn render_stats(&self) -> Result<[init::Region;2]> { + if self.profile==super::m3_profile::Profile::Legacy { + let value=self.stats_region()?;return Ok([value;2]); + } + let region=|index:usize,offset:usize| { + let object=&self.objects[index]; + init::Region::new(object.va().checked_add(offset as u64).ok_or(EOVERFLOW)?, + object.size().checked_sub(offset).ok_or(EINVAL)?).map_err(|_|EINVAL) + }; + Ok([region(UNKNOWN_C0,4)?,region(UNKNOWN_C1,8)?]) + } + pub(crate) fn stats_region(&self) -> Result { - let owner = &self.objects[HARDWARE_DATA]; + let owner = &self.objects[if self.profile == super::m3_profile::Profile::Extended { + UNKNOWN_C3 + } else { HARDWARE_DATA }]; init::Region::new(owner.va(), owner.size()).map_err(|_| EINVAL) } pub(crate) fn log_active_power(&mut self, dev: &driver::AsahiDrmDevice, device: &crate::m3::m3_device::Device, sequence: u32) -> Result { + if self.profile == super::m3_profile::Profile::Extended { + let state = device.pstate_register()? & 15; + let performance = &mut self.objects[super::m3_init_26::PERFORMANCE_CONTROL]; + let actual = performance.read_u32(0x2c)?; + let target = performance.read_u32(0x30)?; + let hardware = performance.read_u32(0x38)?; + dev_info!(dev.as_ref(), "M3_NATIVE_POWER sample={} state={} actual={} target={} hardware={}\n", + sequence, state, actual, target, hardware); + return Ok(()); + } let cap = self.thermal.allowed(kernel::time::Instant::::now()); self.pstates.log_active(dev.as_ref().as_ref(), &mut self.objects[HARDWARE_DATA], device.pstate_register()?, cap, sequence)?; @@ -229,6 +297,7 @@ impl Config { } pub(crate) fn render_pb(&mut self) -> Result { + if self.profile == super::m3_profile::Profile::Extended { return Err(EOPNOTSUPP); } self.objects[UNKNOWN_C1].write(0,&storage::parameter_buffer(crate::m3::m3_render::PB_PAGES).map_err(|_|EINVAL)?) } pub(crate) fn submit_queue(&mut self, kind:usize, queue:u64, head:u16,event:u8,new:bool)->Result { @@ -261,6 +330,9 @@ impl Config { Ok(true) } pub(crate) fn ready(&mut self) -> Result { + if self.profile == super::m3_profile::Profile::Extended { + return Ok(self.objects[RUNTIME_FLAGS].read_u32(0x14)? == 1); + } // NewRegionB+4 is an input initialized to 1, not an acknowledgement. // These are firmware-written transitions present in compute-15's // pre-submit snapshot; both start at zero in the exported schema. @@ -268,6 +340,13 @@ impl Config { && self.objects[RUNTIME_FLAGS].read_u32(4)? == 1) } pub(crate) fn log_ready(&mut self, dev: &driver::AsahiDrmDevice) -> Result { + if self.profile == super::m3_profile::Profile::Extended { + let ready = self.objects[RUNTIME_FLAGS].read_u32(0x14)?; + let consumed = self.objects[CONTROL_REGION].read_u32(0xeae8)?; + dev_info!(dev.as_ref(), "M3: extended firmware ready={} control-generation={}/{}\n", + ready, consumed, self.control_generation); + return Ok(()); + } let magic=self.objects[RUNTIME_POINTERS].read_u32(0x314)?; let state=self.objects[RUNTIME_POINTERS].read_u32(0x2fc)?; let a=self.objects[RUNTIME_FLAGS].read_u32(4)?; @@ -278,12 +357,14 @@ impl Config { } /// The firmware's halt counter and its halted and resume flags. pub(crate) fn halt_state(&mut self) -> Result<(u64,u32,u32)> { + if self.profile == super::m3_profile::Profile::Extended { return Err(EOPNOTSUPP); } let control=&mut self.objects[CONTROL_REGION]; Ok((control.read_u64(0x4580)?,control.read_u32(0x4590)?,control.read_u32(0x45a0)?)) } /// Let a halted firmware continue: clear halted, then set resume. These /// words keep the halt/resume layout of earlier Apple GPU firmware. pub(crate) fn resume_halted(&mut self) -> Result { + if self.profile == super::m3_profile::Profile::Extended { return Err(EOPNOTSUPP); } let control=&mut self.objects[CONTROL_REGION]; control.u32(0x4590,0)?; agx_memory::publish(); @@ -297,6 +378,18 @@ impl Config { /// at +0x40a0, and the details it leaves for a recovery event at +0x44a0. /// Nothing is acknowledged, cleared or resumed here. pub(crate) fn log_recovery_state(&mut self, dev: &driver::AsahiDrmDevice, events: u64) -> Result { + if self.profile == super::m3_profile::Profile::Extended { + for (owner, start, length) in [(super::m3_init_26::RECOVERY_ACTIVITY, 0, 0x60), + (CONTROL_REGION, 0x4ebc, 0x100)] { + for offset in (start..start + length).step_by(32) { + let mut bytes = [0; 32]; + self.objects[owner].read(offset, &mut bytes)?; + dev_err!(dev.as_ref(), "M3: firmware recovery owner={} offset={:#x} bytes={:02x?}\n", + owner, offset, bytes); + } + } + return Ok(()); + } let control=&mut self.objects[CONTROL_REGION]; let halt_count=control.read_u64(0x4580)?; let halted=control.read_u32(0x4590)?; @@ -312,7 +405,7 @@ impl Config { /// index (at most 16), as `drain` reads them. Nothing is consumed or acknowledged, and the /// completion count is not changed. pub(crate) fn log_event_ring(&mut self, dev: &driver::AsahiDrmDevice) -> Result { - const SIZE: usize = 0x38; + let size = if self.profile == super::m3_profile::Profile::Extended { 0x48 } else { 0x38 }; const COUNT: usize = 256; let r = self.objects[storage::EVENT*2].read_u32(0)? as usize; let w = self.objects[storage::EVENT*2].read_u32(0x20)? as usize; @@ -321,8 +414,8 @@ impl Config { let mut index = r; for _ in 0..16 { if index == w { break; } - let mut raw = [0u8; SIZE]; - self.objects[storage::EVENT*2+1].read(index * SIZE, &mut raw)?; + let mut raw = [0u8; 0x48]; + self.objects[storage::EVENT*2+1].read(index * size, &mut raw[..size])?; dev_info!(dev.as_ref(), "M3 event ring entry {} kind={} bytes={:02x?}\n", index, u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]), raw); index = (index + 1) % COUNT; @@ -343,10 +436,40 @@ impl Config { agx_memory::publish(); Ok(next) } + /// Publish an extended-format synchronous control. The caller rings the + /// doorbell after this method and waits for both retirement cursors. + pub(crate) fn control_extended(&mut self, action: super::m3_channel_format::Action) -> Result { + use super::m3_channel_format as format; + if self.profile != super::m3_profile::Profile::Extended { return Err(EINVAL); } + let state = storage::DEVICE_CONTROL * 2; + let cursors = [self.objects[state].read_u32(0)?, + self.objects[state].read_u32(0x10)?, self.objects[state].read_u32(0x20)?]; + let cursor = format::idle_cursor(cursors).map_err(|error| { + if error == init::Error::Bounds { EIO } else { EBUSY } + })?; + let consumed = self.objects[CONTROL_REGION].read_u32(0xeae8)?; + let generation = action.generation(self.control_generation, consumed).map_err(|_| EOVERFLOW)?; + let mut record = [0; format::CONTROL_BYTES]; + action.encode(&mut record).map_err(|_| EINVAL)?; + self.objects[state + 1].write(cursor as usize * format::CONTROL_BYTES, &record)?; + if generation != 0 { + self.objects[GLOBALS].write(0xe41, &generation.to_le_bytes())?; + } + agx_memory::publish(); + let next = (cursor + 1) % format::SLOTS; + self.objects[state].u32(0x20, next)?; + agx_memory::publish(); + self.control_generation = generation; + Ok(next) + } + pub(crate) fn control_done(&mut self, next: u32) -> Result { Ok(self.objects[storage::DEVICE_CONTROL*2].read_u32(0)? == next && self.objects[storage::DEVICE_CONTROL*2].read_u32(0x10)? == next) } pub(crate) fn drain(&mut self, dev: &driver::AsahiDrmDevice) -> Result { + if self.profile == super::m3_profile::Profile::Extended { + return self.drain_extended(dev); + } // Event, trace, stats and six independent firmware log rings. for (state, ring, size, count, channels) in [(storage::EVENT*2,storage::EVENT*2+1,0x38,256,1), (storage::TRACE*2,storage::TRACE*2+1,0x38,512,1), @@ -392,4 +515,147 @@ impl Config { } Ok(()) } + fn drain_extended(&mut self, dev: &driver::AsahiDrmDevice) -> Result { + use super::m3_channel_format as format; + // Snapshot each producer once. New arrivals remain for the next drain; + // a corrupt cursor is rejected before reading its ring allocation. + for (channel, capacity, threads) in [(storage::FIRMWARE_LOG, 256, 9), + (storage::EVENT, 256, 1), (storage::TRACE, 512, 1), (storage::STATS, 256, 1)] { + let state = channel * 2; + for thread in 0..threads { + let at = thread * 0x30; + let mut read = self.objects[state].read_u32(at)?; + let write = self.objects[state].read_u32(at + 0x20)?; + if read >= capacity || write >= capacity { return Err(EIO); } + let before = read; + while read != write { + let offset = (thread * capacity as usize + read as usize) * format::RECEIVE_BYTES; + let mut record = [0; format::RECEIVE_BYTES]; + self.objects[state + 1].read(offset, &mut record)?; + let kind = u32::from_le_bytes(record[..4].try_into().map_err(|_| EIO)?); + if channel == storage::FIRMWARE_LOG { + let slot = u32::from_le_bytes(record[8..12].try_into().map_err(|_| EIO)?); + let payload = format::payload_offset(thread, slot).map_err(|_| EIO)?; + if crate::debug::debug_enabled(crate::debug::DebugFlags::KTraceCh) { + let mut text = [0; 200]; + self.objects[FWLOG_PAYLOAD].read(payload + 0x10, &mut text)?; + let len = text.iter().position(|value| *value == 0).unwrap_or(text.len()); + dev_info!(dev.as_ref(), "M3 firmware log[{}]: {}\n", thread, + core::str::from_utf8(&text[..len]).unwrap_or("")); + } + } else if channel == storage::EVENT { + if kind != 1 { + dev_err!(dev.as_ref(), "M3 firmware event {}: {:02x?}\n", kind, record); + return Err(EIO); + } + let flags = u32::from_le_bytes(record[4..8].try_into().map_err(|_| EIO)?); + self.completed_events = self.completed_events.saturating_add((flags & 7).count_ones().into()); + } else if (channel == storage::TRACE && kind != 5) + || (channel == storage::STATS && kind == 0x16) { + dev_err!(dev.as_ref(), "M3 firmware channel {} record {}: {:02x?}\n", channel, kind, record); + return Err(EIO); + } + read = (read + 1) % capacity; + } + if read != before { self.objects[state].u32(at, read)?; } + } + } + agx_memory::publish(); + Ok(()) + } + +} + +/// All extended initialization owners, retained as one construction transaction. +/// Nothing is published to RTKit by this constructor. A failed allocation drops +/// its mappings before backing storage and leaves no root visible to firmware. +pub(crate) struct ExtendedStorage { + pub(crate) objects: KVec, + pub(crate) iomaps: KVec, + pub(crate) descriptors: KVec, + pub(crate) maximum_khz: u32, +} + +impl ExtendedStorage { + pub(crate) fn new( + dev: &driver::AsahiDrmDevice, + uat: &mmu::Uat, + pwr: &super::hw::PwrConfig, + sram_uv: &[u32], + bases: super::m3_mmio_26::RegisterBases, + ) -> Result { + use super::{m3_init_26 as plan, m3_mmio_26 as io}; + let mut objects = KVec::new(); + for index in 0..plan::COUNT { + let entry = plan::allocation(index).map_err(|_| EINVAL)?; + let alias = match index { + plan::GLOBAL_POOL => Some((uat.kernel_lower_vm(), 0x70_0000_0000)), + plan::GLOBAL_PARAMETERS => Some((uat.kernel_lower_vm(), 0x70_0000_8000)), + _ => None, + }; + let object = if plan::cached(index) { + Buffer::at_prot(dev, uat.kernel_vm(), alias, Some(entry.address), entry.size, + prot::PROT_FW_PRIV_RW, prot::PROT_GPU_SHARED_RW)? + } else { + Buffer::at(dev, uat.kernel_vm(), alias, Some(entry.address), entry.size, entry.gpu_shared)? + }; + objects.push(object, GFP_KERNEL)?; + } + let mut descriptors = KVec::new(); + descriptors.resize(io::SLOTS * io::RECORD_BYTES, 0, GFP_KERNEL)?; + let mut iomaps = KVec::new(); + for slot in 0..io::SLOTS { + let mut region = None; + if let Some(area) = io::area(slot, bases).map_err(|_| EINVAL)? { + let address = 0xffff_fc23_0000_0000 + slot as u64 * 0x10_0000; + let access = if area.writable() { prot::PROT_FW_MMIO_RW } else { prot::PROT_FW_MMIO_RO }; + for (physical, bytes, offset) in area.banks() { + if bytes == 0 { continue; } + let mapping = uat.kernel_vm().map_io_m3(address + offset as u64, + physical.try_into()?, bytes, access)?; + iomaps.push(mapping, GFP_KERNEL)?; + } + region = Some(init::Region::new(address, area.mapped_bytes()).map_err(|_| EINVAL)?); + } + io::encode(slot, bases, region, + &mut descriptors[slot * io::RECORD_BYTES..(slot + 1) * io::RECORD_BYTES]) + .map_err(|_| EINVAL)?; + } + // Runtime+0x18 references Fender SRAM, rather than anonymous DMA RAM. + // Retain its normal shared mapping independently of its MMIO view. + let scratch_physical = bases.gpu.checked_add(0xd6_0000).ok_or(EOVERFLOW)?; + let scratch = uat.kernel_vm().map_io_m3(0xffff_fc22_0000_0000, + scratch_physical.try_into()?, 0x8000, prot::PROT_FW_SHARED_RW)?; + let scratch_region = init::Region::new(scratch.iova(), 0x8000).map_err(|_| EINVAL)?; + iomaps.push(scratch, GFP_KERNEL)?; + let mut regions = [init::Region::new(0xffff_fc21_0000_0000, 1).map_err(|_| EINVAL)?; plan::COUNT]; + for (index, object) in objects.iter().enumerate() { + regions[index] = init::Region::new(object.va(), object.size()).map_err(|_| EINVAL)?; + } + let graph = plan::PointerGraph { owners: ®ions, scratch: scratch_region, + pool_alias: objects[plan::GLOBAL_POOL].gpu_va()?, + parameter_alias: objects[plan::GLOBAL_PARAMETERS].gpu_va()? }; + let mut root = [0; 0xc8]; + let mut runtime = [0; plan::RUNTIME_SIZE]; + let mut control = KVec::new(); + control.resize(plan::allocation(CONTROL_REGION).map_err(|_| EINVAL)?.size, 0, GFP_KERNEL)?; + graph.root(&mut root).map_err(|_| EINVAL)?; + graph.runtime(&mut runtime).map_err(|_| EINVAL)?; + graph.firmware_control(&mut control).map_err(|_| EINVAL)?; + let images = super::m3_power_26::PowerImages::new(pwr, sram_uv, + objects[plan::SENSOR_VALUE].va(), objects[plan::RETENTION_STATE].va(), + agx_memory::TIMESTAMP_RANGE.start, &descriptors)?; + objects[storage::ROOT].write(0, &root)?; + objects[RUNTIME_POINTERS].write(0, &runtime)?; + objects[CONTROL_REGION].write(0, &control)?; + objects[HARDWARE_DATA].write(0, &images.hardware)?; + objects[plan::PERFORMANCE_CONTROL].write(0, &images.performance)?; + objects[GLOBALS_POWER].write(0, &images.power)?; + objects[GLOBALS].write(0, &images.globals)?; + // Fragment scheduler entries start unassigned; these are input sentinels. + objects[UNKNOWN_C1].u32(0xc18, u32::MAX)?; + objects[UNKNOWN_C1].u32(0xc30, u32::MAX)?; + agx_memory::publish(); + Ok(Self { objects, iomaps, descriptors, maximum_khz: images.maximum_khz }) + } } diff --git a/drivers/gpu/drm/asahi/m3/m3_device.rs b/drivers/gpu/drm/asahi/m3/m3_device.rs index c43017a8933c0f..474a8b3f561ec7 100644 --- a/drivers/gpu/drm/asahi/m3/m3_device.rs +++ b/drivers/gpu/drm/asahi/m3/m3_device.rs @@ -129,6 +129,7 @@ impl Device { } e.sgx_setup() } + None if device.firmware.profile() == super::m3_profile::Profile::Extended => None, None => Some(soc.sgx_setup.ok_or(ENODEV)?), }; if let Some((setup_offset, setup_value)) = setup { @@ -176,6 +177,11 @@ impl Device { /// Read-only snapshot. Do not select or acknowledge fault banks here; /// the firmware owns those registers until recovery is ported. pub(crate) fn log_engine_state(&self, vm: &crate::mmu::Vm) -> Result { + if self.firmware.profile() == super::m3_profile::Profile::Extended { + // Adaptive clock gating can leave individual engine banks inaccessible + // even while the GPU performance-state register says powered up. + return Ok(()); + } let sgx_guard = self.sgx.try_access().ok_or(ENODEV)?; let sgx = sgx_guard.as_view(); let pstate = sgx.try_read32(0xe01000)? & 0xf; diff --git a/drivers/gpu/drm/asahi/m3/m3_drm.rs b/drivers/gpu/drm/asahi/m3/m3_drm.rs index bcb9fd32c7d255..922d76ba2055bc 100644 --- a/drivers/gpu/drm/asahi/m3/m3_drm.rs +++ b/drivers/gpu/drm/asahi/m3/m3_drm.rs @@ -86,6 +86,7 @@ pub(crate) struct Backend { core_mask: u64, max_frequency_khz: u32, soc: &'static crate::m3::m3_soc::Soc, + profile: super::m3_profile::Profile, _progress: Pin>>, _activity: Pin>>, _memory: Pin>>, @@ -112,10 +113,12 @@ impl Backend { crate::m3::t8122_start::not_admitted(pdev.as_ref(), soc); })?; dev_info!(pdev.as_ref(), "M3: resource admission complete\n"); - // An armed T8122 start supplies the values its table leaves out. An unarmed T8122 - // needs a complete table, which it does not have: it is refused here. - let experiment = crate::m3::t8122_start::arm(pdev.as_ref(), soc)?; - if experiment.is_none() { + // Extended T8122 uses its dedicated initialization graph. Keep the + // legacy guarded-start and completeness checks for older interfaces. + let extended=crate::m3::t8122_start::is_t8122(soc) + && crate::m3::t8122_admission::loaded_profile(pdev)?==super::m3_profile::Profile::Extended; + let experiment = if extended {None} else {crate::m3::t8122_start::arm(pdev.as_ref(), soc)?}; + if experiment.is_none() && !extended { soc.require_complete(pdev.as_ref())?; } let experiment = experiment.as_ref(); @@ -125,6 +128,7 @@ impl Backend { let firmware = crate::m3::m3_firmware::identify_loaded(pdev, soc, resources, experiment) .inspect_err(refused("the firmware identity"))?; dev_info!(pdev.as_ref(), "M3: firmware identity accepted\n"); + let profile = firmware.profile(); // InitData source and contents, before any GPU register access. let contents = crate::m3::m3_adt_config::Contents::select(pdev, &firmware, soc, experiment) .inspect_err(refused("the InitData contents"))?; @@ -160,7 +164,7 @@ impl Backend { let completion_work = crate::m3::m3_completion::Completion::new(shared.clone())?; // From here on every failure drops the backend, which stops the runtime. let backend = Arc::new(Self { shared, health, ids: gpu::SequenceIDs::default(), - scheduler, core_mask, max_frequency_khz, soc, + scheduler, core_mask, max_frequency_khz, soc, profile, _progress: progress, _activity: activity, _memory: memory, _timing: timing, _completion: completion, _overlap: overlap }, GFP_KERNEL)?; // The completion worker services this runtime from now on; the RTKit callbacks and @@ -208,7 +212,9 @@ impl gpu::Gpu for Backend { } params.max_frequency_khz = self.max_frequency_khz; params.usc_generation = 3; - params.gpu_hal_generation = hw::GpuHalGeneration::Legacy as u32; + params.gpu_hal_generation = self.profile.hal(); + params.max_commands_per_submission = super::agx_uapi::UAPI_MAX_HARDWARE_COMMANDS; + params.max_attachments = super::agx_uapi::UAPI_MAX_ATTACHMENTS as u32; Ok(()) } fn supports_vm_status(&self) -> bool { true } @@ -217,10 +223,12 @@ impl gpu::Gpu for Backend { fn feature_flags(&self) -> u64 { let mut flags = uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_VM_STATUS as u64 | uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_SCHEDULED_QUEUES as u64; - if self.soc.features.fragment_dependency && crate::m3::m3_params::early_tiling() { + if self.profile == super::m3_profile::Profile::Legacy + && self.soc.features.fragment_dependency && crate::m3::m3_params::early_tiling() { flags |= uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_FRAGMENT_DEPENDENCY as u64; } - if self.soc.features.compute_wide_visibility { + if self.profile == super::m3_profile::Profile::Legacy + && self.soc.features.compute_wide_visibility { flags |= uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_COMPUTE_WIDE_VISIBILITY as u64; } flags @@ -233,17 +241,17 @@ impl gpu::Gpu for Backend { } fn uat_geometry(&self) -> mmu::UatGeometry { mmu::UatGeometry { ias: 42, oas: 42 } } fn user_range(&self) -> Range { - mmu::UAT_PGSZ as u64..crate::m3::initdata::RUNTIME_UNKNOWN_PAGE + mmu::UAT_PGSZ as u64..self.profile.user_end() } /// The dummy page sits where InitData tells the firmware it is (unk_30), as on 7.1.12; the /// 42-bit geometry's own unk_page() is a different address. fn unknown_page(&self) -> u64 { - crate::m3::initdata::RUNTIME_UNKNOWN_PAGE + self.profile.user_end() } fn tracked_submission(&self) -> bool { true } /// Only M3-aware userspace may create a VM (7.1.12's per-file client gate). fn admit_vm(&self, dev: &kernel::device::Device, params_size: u64, flags: u32) -> Result { - crate::m3::m3_client::admit_vm(dev, params_size, flags) + crate::m3::m3_client::admit_vm_for(dev, self.profile, params_size, flags) } fn new_vm(&self, range: Range) -> Result { Option::as_mut(&mut *self.shared.lock()).ok_or(ENODEV)?.new_vm(self.ids.vm.next(), range) @@ -252,7 +260,8 @@ impl gpu::Gpu for Backend { _ualloc_priv: Arc>, priority: u32, usc_exec_base: u64) -> Result> { Ok(KBox::new(crate::m3::m3_submit::Queue::new(self.shared.clone(),self.scheduler.clone(),vm,priority,usc_exec_base, - self.soc.features.fragment_dependency)?,GFP_KERNEL)?) + self.feature_flags() & uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_FRAGMENT_DEPENDENCY as u64 != 0, + self.feature_flags() & uapi::drm_asahi_feature_DRM_ASAHI_FEATURE_COMPUTE_WIDE_VISIBILITY as u64 != 0)?,GFP_KERNEL)?) } fn map_timestamp_buffer(&self, bo: gem::ObjectRef, range: Range) -> Result { crate::timestamp::Buffer::firmware( diff --git a/drivers/gpu/drm/asahi/m3/m3_firmware.rs b/drivers/gpu/drm/asahi/m3/m3_firmware.rs index 0d2835174c6775..94a2d3384d08aa 100644 --- a/drivers/gpu/drm/asahi/m3/m3_firmware.rs +++ b/drivers/gpu/drm/asahi/m3/m3_firmware.rs @@ -62,6 +62,19 @@ pub(crate) static KNOWN_IMAGES_T8122: [KnownImage; 1] = [KnownImage { initdata_magic: Some(G15_V14_8_3_INITDATA), }]; +/// Exact normalized image identity for the extended firmware interface. +pub(crate) static T8122_IMAGES_26: [KnownImage; 1] = [KnownImage { + name: "T8122 C0 firmware-compat 26.6.2", + uuid: None, + stkg_sha256: Some([ + 0x3a, 0x6b, 0x8b, 0x5e, 0x83, 0x38, 0x4d, 0x5c, + 0xc1, 0x92, 0x9d, 0x91, 0xb6, 0x36, 0x40, 0xfe, + 0xe8, 0xf5, 0x52, 0xb5, 0x10, 0x8c, 0x4e, 0xa6, + 0x20, 0xbe, 0xdb, 0x7b, 0xbc, 0xa0, 0x31, 0x64, + ]), + initdata_magic: Some(0x0c89_c357_8392_04b8), +}]; + /// The same T8122 image for the start experiment (`t8122_start`, only with /// `asahi.t8122_start=1`), where `asahi.t8122_initdata_version` may replace the version. pub(crate) static KNOWN_IMAGES_T8122_EXPERIMENT: [KnownImage; 1] = [KnownImage { @@ -117,6 +130,10 @@ pub(crate) struct Layout { /// Offset of the 8-byte tag that precedes the 8 per-boot bytes zeroed before hashing. pub(crate) entropy_tag_offset: usize, pub(crate) entropy_tag: [u8; 8], + /// An additional independent boot value, when required by this image. + extra_boot_value: Option<(usize, [u8; 8])>, + /// Expected boot-table location and byte count in the image header. + boot_table: Option<(u32, u32)>, /// The firmware version, for logs. pub(crate) version: &'static str, } @@ -127,6 +144,8 @@ pub(crate) static T6030_LAYOUT: Layout = Layout { vas: [0xffff_fc00_0000_0000, 0xffff_fc00_0005_c000], entropy_tag_offset: 0x58e88, entropy_tag: *b"GKTS\x08\x00\x00\x00", + extra_boot_value: None, + boot_table: None, version: "RTKit-2419.140.12.release", }; @@ -137,23 +156,73 @@ pub(crate) static T8122_LAYOUT: Layout = Layout { vas: [0xffff_fc00_0000_0000, 0xffff_fc00_0006_4000], entropy_tag_offset: 0x5d54a, entropy_tag: *b"GKTS\x08\x00\x00\x00", + extra_boot_value: None, + boot_table: None, version: "RTKit-2419.140.12.release", }; pub(crate) const TEXT_SHA256: [u8; 32] = [ 0x11, 0xe4, 0x9f, 0x75, 0xb, 0x67, 0x1a, 0x2b, 0x13, 0xd0, 0x92, 0xdb, 0x1c, 0xbc, 0xa3, 0xa8, 0x77, 0x55, 0x86, 0xd6, 0xb1, 0xe3, 0xaa, 0xe8, 0xa6, 0xa4, 0x64, 0xa7, 0x80, 0x2, 0x93, 0xd5]; +/// T8122 C0, firmware-compat 26.6.2 segment and loader-mutable bounds. +pub(crate) static T8122_LAYOUT_26: Layout = Layout { + text_size: 393216, + data_size: 1228800, + vas: [0xffff_fc00_0000_0000, 0xffff_fc00_0006_0000], + entropy_tag_offset: 380492, + entropy_tag: *b"GKTS\x08\x00\x00\x00", + extra_boot_value: Some((380719, *b"ECAP\x08\x00\x00\x00")), + boot_table: Some((380492, 561)), + version: "RTKit-3255.160.4.release", +}; + fn normalize_boot_entropy(layout: &Layout, text: &mut [u8]) -> bool { - let tag = layout.entropy_tag_offset; if text.len() != layout.text_size as usize { return false; } - let Some(end) = tag.checked_add(16) else { return false; }; - let Some(entropy) = text.get_mut(tag..end) else { return false; }; - if entropy[..8] != layout.entropy_tag { - return false; + if let Some((offset, count)) = layout.boot_table { + let Some(header) = text.get(0x22c..0x234) else { + return false; + }; + if header[..4] != offset.to_le_bytes() || header[4..] != count.to_le_bytes() { + return false; + } + let Some(end) = (offset as usize).checked_add(count as usize) else { + return false; + }; + if text.get(offset as usize..end).is_none() { + return false; + } + } + let fields = [ + Some((layout.entropy_tag_offset, layout.entropy_tag)), + layout.extra_boot_value, + ]; + // Validate every field before touching even the first boot value. + for (offset, tag) in fields.into_iter().flatten() { + let Some(end) = offset.checked_add(16) else { + return false; + }; + let Some(field) = text.get(offset..end) else { + return false; + }; + if field[..8] != tag { + return false; + } + if let Some((base, count)) = layout.boot_table { + if offset < base as usize || end > base as usize + count as usize { + return false; + } + } + } + if let Some((other, _)) = layout.extra_boot_value { + if other < layout.entropy_tag_offset + 16 && layout.entropy_tag_offset < other + 16 { + return false; + } + } + for (offset, _) in fields.into_iter().flatten() { + text[offset + 8..offset + 16].fill(0); } - entropy[8..].fill(0); true } @@ -167,6 +236,14 @@ pub(crate) struct Firmware { } impl Firmware { + pub(crate) fn profile(&self) -> super::m3_profile::Profile { + if core::ptr::eq(self.layout, &T8122_LAYOUT_26) { + super::m3_profile::Profile::Extended + } else { + super::m3_profile::Profile::Legacy + } + } + pub(crate) const fn version(&self) -> &'static str { self.layout.version } @@ -178,6 +255,51 @@ impl Firmware { } } +/// Only the admitted extended image has this host-owned boot descriptor slot. +fn loader_span(profile: super::m3_profile::Profile, data_bytes: u64, input_bytes: usize) + -> Option> { + if profile != super::m3_profile::Profile::Extended || input_bytes != 53 * 40 + || data_bytes != T8122_LAYOUT_26.data_size { return None; } + let start: usize = 0x12_28e0; + let end = start.checked_add(input_bytes)?; + if end as u64 > data_bytes { return None; } + Some(start..end) +} + +#[cfg(not(test))] +impl Firmware { + /// Call after RTKit application startup and before exposing the InitData root. + /// The protocol grants the host this slot only during that boot interval. + pub(crate) fn publish_io_table( + &self, + pdev: &kernel::platform::Device>, + descriptors: &[u8], + ) -> kernel::error::Result { + use kernel::{c_str, io::mem::{Mem, MemFlag}, prelude::*}; + let data = self.resources.regions[5]; + let span = loader_span(self.profile(), data.size, descriptors.len()).ok_or(EINVAL)?; + let node = pdev.as_ref().of_node().ok_or(ENODEV)?; + let resource = agx_resources::reserved_resource(&node, c_str!("fw-data"))?; + if resource.start() != data.base || resource.size() != data.size { return Err(EINVAL); } + // SAFETY: This exact admitted no-map reservation remains firmware-owned. + // Only its bounded host boot slot is accessed, before root publication. + let mapping = unsafe { Mem::try_new(resource, MemFlag::WC.into()) }?; + if span.end > mapping.size() { return Err(EINVAL); } + // Validate the whole slot before changing any byte. Unexpected prior + // contents indicate a protocol/ownership mismatch, not a patchable image. + for offset in span.clone() { + // SAFETY: span is bounded by the retained mapping above. + if unsafe { mapping.ptr().add(offset).read_volatile() } != 0 { return Err(EBUSY); } + } + for (offset, byte) in span.zip(descriptors.iter().copied()) { + // SAFETY: same bounded host slot, exclusive under the boot protocol. + unsafe { mapping.ptr().add(offset).write_volatile(byte) }; + } + crate::m3::agx_memory::publish(); + Ok(()) + } +} + #[cfg(not(test))] pub(crate) fn identify_loaded( pdev: &kernel::platform::Device>, @@ -191,7 +313,10 @@ pub(crate) fn identify_loaded( prelude::*, }; - let layout = soc.firmware.ok_or(ENODEV)?; + let extended=crate::m3::t8122_start::is_t8122(soc) + && crate::m3::t8122_admission::loaded_profile(pdev)?==super::m3_profile::Profile::Extended; + if extended && experiment.is_some() {return Err(EINVAL);} + let layout = if extended {&T8122_LAYOUT_26} else {soc.firmware.ok_or(ENODEV)?}; if !Firmware::matching_layout(layout, &resources) { dev_err!(pdev.as_ref(), "M3 {}: unsupported firmware segment layout\n", soc.gpu_name); return Err(ENODEV); @@ -215,6 +340,7 @@ pub(crate) fn identify_loaded( // The T8122 start experiment identifies the same image, with its own InitData version. let images = match experiment { Some(_) => &KNOWN_IMAGES_T8122_EXPERIMENT[..], + None if extended => &T8122_IMAGES_26[..], None => soc.images, }; let image = crate::m3::m3_board::identify(pdev.as_ref(), bytes, &digest, images).ok_or(ENODEV)?; @@ -393,4 +519,75 @@ mod tests { assert!(!normalize_boot_entropy(&layout, &mut text)); assert_eq!(text, vec![0xa5; 15]); } + + fn image_26() -> Vec { + let mut image = vec![0x5a; 393216]; + image[0x22c..0x230].copy_from_slice(&380492u32.to_le_bytes()); + image[0x230..0x234].copy_from_slice(&561u32.to_le_bytes()); + image[380492..380500].copy_from_slice(b"GKTS\x08\x00\x00\x00"); + image[380719..380727].copy_from_slice(b"ECAP\x08\x00\x00\x00"); + image + } + + #[test] + fn newer_image_masks_only_both_boot_values() { + let mut image = image_26(); + let before = image.clone(); + assert!(normalize_boot_entropy(&T8122_LAYOUT_26, &mut image)); + for index in 0..image.len() { + let expected = if (380500..380508).contains(&index) || (380727..380735).contains(&index) + { + 0 + } else { + before[index] + }; + assert_eq!(image[index], expected, "byte {index}"); + } + } + + #[test] + fn invalid_second_field_or_table_does_not_mask_first_value() { + for index in [0x22c, 0x230, 380492, 380496, 380719, 380723] { + let mut image = image_26(); + image[index] ^= 1; + let before = image.clone(); + assert!(!normalize_boot_entropy(&T8122_LAYOUT_26, &mut image)); + assert_eq!(image, before); + } + let mut image = image_26(); + image.pop(); + let before = image.clone(); + assert!(!normalize_boot_entropy(&T8122_LAYOUT_26, &mut image)); + assert_eq!(image, before); + } + + #[test] + fn extended_record_cannot_replace_legacy_identity() { + let newer = &T8122_IMAGES_26[0]; + let digest = newer.stkg_sha256.unwrap(); + assert!(newer.accepts_runtime(None, &digest)); + assert_eq!(newer.initdata_magic, Some(0x0c89_c357_8392_04b8)); + for index in 0..digest.len() { + let mut wrong = digest; + wrong[index] ^= 1; + assert!(!newer.accepts_runtime(None, &wrong)); + } + assert!(!KNOWN_IMAGES_T8122[0].accepts_runtime(None, &digest)); + assert!(!newer.accepts_runtime(KNOWN_IMAGES_T8122[0].uuid, &[0; 32])); + } + + #[test] + fn loader_slot_is_exact_bounded_and_profile_specific() { + use super::super::m3_profile::Profile; + let length = 53 * 40; + assert_eq!(loader_span(Profile::Extended, 0x12c000, length), Some(0x1228e0..0x123128)); + for size in [0, 0x123127, 0x12bfff, 0x12c001, u64::MAX] { + assert!(loader_span(Profile::Extended, size, length).is_none()); + } + for bytes in [0, length - 1, length + 1, usize::MAX] { + assert!(loader_span(Profile::Extended, 0x12c000, bytes).is_none()); + } + assert!(loader_span(Profile::Legacy, 0x12c000, length).is_none()); + } + } diff --git a/drivers/gpu/drm/asahi/m3/m3_fragment_command.rs b/drivers/gpu/drm/asahi/m3/m3_fragment_command.rs index 2367e6c7ad557f..0abccad3cb9d64 100644 --- a/drivers/gpu/drm/asahi/m3/m3_fragment_command.rs +++ b/drivers/gpu/drm/asahi/m3/m3_fragment_command.rs @@ -3,6 +3,8 @@ use super::{agx_render::Geometry, agx_render_state::{Program,DepthStencil}, m3_compute_layout::{GpuVa,RegisterSet}, m3_init_layout::{Error,Region}, m3_queue_layout::FirmwareVa, m3_render_sequence::fragment}; pub(crate) const SIZE:usize=0xc73; +pub(crate) const EXTENDED_SIZE:usize=0xca0; +pub(crate) const EXTENDED_FLUSH:usize=0xbdc; pub(crate) const REGISTERS:usize=fragment::REGISTERS; /// The registers G15G does not take; the others keep the G15S order. pub(crate) const G15G_OMIT:[u32;4]=[0x160a8,0x160b8,0xa5a9,0xd429]; @@ -74,16 +76,30 @@ impl Command { /// Check references before touching output. The caller owns validation of /// client resource mappings; this layer serializes their semantic state. pub(crate) fn encode(&self,out:&mut[u8])->Result<(),Error> { - if out.len()Result<(),Error> { + self.encode_format(out,Some(dispatch)) + } + fn encode_format(&self,out:&mut[u8],dispatch:Option)->Result<(),Error> { + let extended=dispatch.is_some(); + let size=if extended {EXTENDED_SIZE} else {SIZE}; + if out.len()=1<<42 || self.register_set!=RegisterSet::G15G {return Err(Error::Address);} + } if self.context>=64 {return Err(Error::Address);} - let seq=self.sequence.at(0,fragment::LENGTH)?; + let sequence_size=if extended {0x370} else {fragment::LENGTH}; + let seq=self.sequence.at(0,sequence_size)?; let alias=self.gpu_alias.get(); - GpuVa::new(alias.checked_add((SIZE-3) as u64).ok_or(Error::Address)?).map_err(|_|Error::Address)?; + GpuVa::new(alias.checked_add(((size-1)&!3) as u64).ok_or(Error::Address)?).map_err(|_|Error::Address)?; let registers=alias+REGISTERS as u64; let s=&self.state;let g=&s.geometry; let layer_metadata=self.heap.get().checked_add(if g.layers>1 {0x200}else{0}).ok_or(Error::Address)?; GpuVa::new(layer_metadata).map_err(|_|Error::Address)?; - let user=super::agx_render_state::compact(self.scene_user.get().checked_add(0x40).ok_or(Error::Address)?) + let user=super::agx_render_state::compact(self.scene_user.get().checked_add(if extended {0} else {0x40}).ok_or(Error::Address)?) .map_err(|_|Error::Address)? & !15; let width=(g.pixels&0xffff)+1;let height=(g.pixels>>16)+1; let tx=(width+31)/32;let ty=(height+31)/32; @@ -116,43 +132,85 @@ impl Command { (0x101c1,0),(0xd469,0),(0x1a0f9,8),(0x1c838,0),(0x1ca28,user), (0xa5a9,0x6000400020),(0xd429,0x200000001), ]; - let bytes=&mut out[..SIZE];bytes.fill(0); + let bytes=&mut out[..size];bytes.fill(0); word(bytes,0,1); // fragment opcode; work counter remains zero word(bytes,0xc,self.context); for (offset,value) in [(0x14,seq),(0x20,self.notifier.get()),(0x28,self.manager.get()), (0x30,self.scene.get()),(0x38,self.empty.get()),(0x40,self.tilemap.get()),(0x780,registers)] {pointer(bytes,offset,value);} - word(bytes,SEQUENCE_LENGTH,fragment::LENGTH as u32); - // The qualified patch path updates register MSAA but retains these - // producer header values. Representation cleanup must not change that. - pointer(bytes,0x48,0x88);word(bytes,0x50,1); - bytes[0x54..0x56].copy_from_slice(&((g.y_blocks>>18) as u16).to_le_bytes()); - bytes[0x56..0x58].copy_from_slice(&((g.x_blocks>>18) as u16).to_le_bytes()); - word(bytes,0x68,s.merge_upper[0]);word(bytes,0x6c,s.merge_upper[1]); - pointer(bytes,0x78,u64::from(g.tiles)); + word(bytes,SEQUENCE_LENGTH,sequence_size as u32); + if extended { + pointer(bytes,4,dispatch.as_ref().map_or(0,|native|native.serial)); + word(bytes,0x50,1); + word(bytes,0x54,g.isp_blocks); + word(bytes,0x60,g.x_blocks); + word(bytes,0x64,g.y_blocks); + word(bytes,0x68,g.blocks); + word(bytes,0x78,g.tiles); + } else { + pointer(bytes,0x48,0x88);word(bytes,0x50,1); + bytes[0x54..0x56].copy_from_slice(&((g.y_blocks>>18) as u16).to_le_bytes()); + bytes[0x56..0x58].copy_from_slice(&((g.x_blocks>>18) as u16).to_le_bytes()); + word(bytes,0x68,s.merge_upper[0]);word(bytes,0x6c,s.merge_upper[1]); + pointer(bytes,0x78,u64::from(g.tiles)); + } let g15g=self.register_set==RegisterSet::G15G; let mut count=0; - for (register,value) in values.into_iter().filter(|(r,_)| !g15g || !G15G_OMIT.contains(r)) { - let n=count;count+=1; - word(bytes,REGISTERS+n*REGISTER_STRIDE,register);pointer(bytes,REGISTERS+n*REGISTER_STRIDE+4,value); + let mut emit=|register,value| { + word(bytes,REGISTERS+count*REGISTER_STRIDE,register); + pointer(bytes,REGISTERS+count*REGISTER_STRIDE+4,value); + count+=1; + }; + for (register,mut value) in values { + if extended { + if matches!(register,0x160b8|0x120a1|0x1c838|0x1ca28) {continue;} + if register==0x10791 {emit(0xa301,0);} + if register==0x16068 {value=(value&!0x3ffff)|0x3717f;} + if matches!(register,0x15109|0x15101) {value&=!3;} + } else if g15g && G15G_OMIT.contains(®ister) {continue;} + emit(register,value); + } + if let Some(native)=&dispatch { + for register in [0x160e0,0x1499,0xa341] {emit(register,fragment::UUID.into());} + emit(0x1c838,0); + emit(0x1ca28,user); + emit(0x10069,native.usc_window); } bytes[0x788..0x78a].copy_from_slice(&(count as u16).to_le_bytes()); bytes[0x78a..0x78c].copy_from_slice(&((count*REGISTER_STRIDE) as u16).to_le_bytes()); // Start3DJobParameters3: mirror records use their actual packed widths. for (offset,value) in [(0,s.depth_bias),(0x10,s.scissor),(0x20,s.query), - (0x168,s.partial_background.resources),(0x170,s.partial_background.address), + (0x168,if extended {s.background.resources}else{s.partial_background.resources}), + (0x170,if extended {s.background.address}else{s.partial_background.address}), (0x198,s.partial_background.resources),(0x1a0,s.partial_background.address), (0x1a8,s.zls_control),(0x1b8,s.depth.base),(0x1c0,s.depth.stride), (0x1c8,s.depth_compression.stride),(0x1e0,s.depth_compression.base), (0x1d0,s.depth.base),(0x1d8,s.depth.base), (0x1e8,s.stencil.base),(0x1f0,s.stencil.stride),(0x200,s.stencil.base),(0x208,s.stencil.base), (0x1f8,s.stencil_compression.stride),(0x210,s.stencil_compression.base), - (0x26c,s.partial_eot.address),(0x28c,s.partial_eot.address),(0x2c0,u64::from(s.depth_dimensions))] { - pointer(bytes,PARAMETERS+offset,value); + (0x26c,if extended {s.eot.address}else{s.partial_eot.address}),(0x28c,s.partial_eot.address),(0x2c0,u64::from(s.depth_dimensions))] { + pointer(bytes,PARAMETERS+offset+if extended && offset<=0x20 {0x10}else{0},value); } for (offset,value) in [(0x1b0,0x04040404),(0x228,s.mirror_tilebuffer_control), (0x230,s.isp_control),(0x238,width),(0x23c,height),(0x240,0x100000), - (0x248,s.mirror_tile_mode),(0x264,s.partial_eot.resources as u32),(0x284,s.partial_eot.resources as u32), + (0x248,s.mirror_tile_mode),(0x264,if extended {s.eot.resources as u32}else{s.partial_eot.resources as u32}),(0x284,s.partial_eot.resources as u32), (0x298,s.depth_clear),(0x29c,s.stencil_clear),(0x2a0,s.sample_size)] {word(bytes,PARAMETERS+offset,value);} + if let Some(native)=dispatch { + pointer(bytes,0xbc0,self.stamp.get()); + pointer(bytes,0xbc8,self.fw_stamp.get()); + word(bytes,0xbd0,self.stamp_value); + word(bytes,0xbd4,1); + word(bytes,EXTENDED_FLUSH,1); + word(bytes,0xbe0,fragment::UUID); + pointer(bytes,0xc10,self.timestamps[0].get()); + pointer(bytes,0xc18,self.timestamps[1].get()); + pointer(bytes,0xc20,self.user_timestamps[0].get()); + pointer(bytes,0xc28,self.user_timestamps[1].get()); + pointer(bytes,0xc46,self.pool.get()); + bytes[0xc4e]=1; + pointer(bytes,0xc57,native.firmware_aux.get()); + bytes[0xc5f]=native.incarnation; + bytes[0xc84]=1; + } else { word(bytes,0xb88,0x67); // EncoderParams.encoder_id word(bytes,0xb90,u32::MAX); // EncoderParams.unk_mask word(bytes,0xb94,u32::from(s.process_empty_tiles)); @@ -167,6 +225,46 @@ impl Command { pointer(bytes,fragment::COMMAND_TAIL+offset,value); } bytes[fragment::COMMAND_TAIL+0x36]=1;bytes[fragment::COMMAND_TAIL+0x4f]=1; + } Ok(()) } } + +#[cfg(test)] +mod extended_tests { + use super::*; + #[test] + fn native_fragment_uses_distinct_program_mirrors_and_completion_tail() { + let fw=|n:u64|FirmwareVa::new(0xffff_fc25_3000_0000+n*0x4000).unwrap(); + let gpu=|n:u64|GpuVa::new(0x10_0300_0000+n*0x4000).unwrap(); + let program=|n:u64|Program{address:0x1_0000_0000+n*0x1000,resources:n}; + let surface=DepthStencil{base:0,stride:0}; + let command=Command {context:3,register_set:RegisterSet::G15G, + sequence:Region::new(fw(0).get(),0x1000).unwrap(),notifier:fw(1),manager:fw(2),scene:fw(3),empty:fw(4), + gpu_alias:GpuVa::new(0x72_1000_4000).unwrap(),tilemap:gpu(0),heap:gpu(1),auxiliary:gpu(2),scene_user:gpu(3), + scratch:gpu(4),pool:fw(5),stamp:fw(6),fw_stamp:fw(7),stamp_value:0x3d000100, + timestamps:[fw(8),fw(9)],user_timestamps:[fw(10),fw(11)], + state:State {geometry:Geometry::new(64,64,super::super::agx_render::Utile::Pixels32, + super::super::agx_render::Utile::Pixels32,1).unwrap(),multisample:0,merge_upper:[0;2], + eot:program(1),background:program(2),partial_eot:program(3),partial_background:program(4), + scissor:0,depth_bias:0,query:0,isp_control:0xc000,tilebuffer_control:1,mirror_tilebuffer_control:1, + tile_mode:0x280,mirror_tile_mode:0x280,depth:surface,stencil:surface,depth_compression:surface, + stencil_compression:surface,zls_control:0,depth_dimensions:0,depth_clear:0,stencil_clear:0, + sample_size:4,process_empty_tiles:false}}; + let dispatch=||super::super::m3_tiler_command::Dispatch26 {incarnation:9,serial:1,peer_stamp:0, + usc_window:1<<32,firmware_aux:fw(12),scene_scratch:gpu(4)}; + let mut bytes=[0xa5;EXTENDED_SIZE+8]; + command.encode_extended(dispatch(),&mut bytes).unwrap(); + let word=|at|u32::from_le_bytes(bytes[at..at+4].try_into().unwrap()); + let pointer=|at|u64::from_le_bytes(bytes[at..at+8].try_into().unwrap()); + assert_eq!(word(0xbd0),command.stamp_value); + assert_eq!(word(EXTENDED_FLUSH),1); + assert_eq!(pointer(0x900),command.state.background.address); + assert_eq!(pointer(0x930),command.state.partial_background.address); + assert_eq!(bytes[0xc5f],9); + assert_eq!(&bytes[EXTENDED_SIZE..],&[0xa5;8]); + let mut short=[0xa5;EXTENDED_SIZE-1]; + assert!(command.encode_extended(dispatch(),&mut short).is_err()); + assert!(short.iter().all(|b|*b==0xa5)); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_init_26.rs b/drivers/gpu/drm/asahi/m3/m3_init_26.rs new file mode 100644 index 00000000000000..a86432b2bbe2b5 --- /dev/null +++ b/drivers/gpu/drm/asahi/m3/m3_init_26.rs @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Allocation and pointer records for the extended M3 initialization format. +//! Owners supply mapped regions; these builders never allocate or publish DMA. + +use super::m3_init_layout::{Error, Region, Root}; +use super::m3_init_storage::{self as storage, Allocation, Initial}; + +pub(crate) const COUNT: usize = 56; +pub(crate) const DYNAMIC_STATE: usize = 49; +pub(crate) const PERFORMANCE_CONTROL: usize = 50; +pub(crate) const RECOVERY_ACTIVITY: usize = 51; +pub(crate) const SENSOR_VALUE: usize = 52; +pub(crate) const RETENTION_STATE: usize = 53; +pub(crate) const GLOBAL_POOL: usize = 54; +pub(crate) const GLOBAL_PARAMETERS: usize = 55; +pub(crate) const RUNTIME_SIZE: usize = 0x4b8; +pub(crate) const LOG_THREADS: usize = 9; +pub(crate) const RX_BYTES: usize = 0x48; +pub(crate) const LOG_PAYLOAD_BYTES: usize = 0xd8; + +/// Extend the existing M3 owner indices instead of introducing another owner graph. +pub(crate) fn allocation(index: usize) -> Result { + let size = match index { + 0..=23 if index % 2 == 0 => 0x30, + 0..=23 => 0x1800, + 24 | 26 | 30 | 32 => 0x30, + 25 => 0x4000, + 27 | 33 => 0x4800, + 28 => LOG_THREADS * 0x30, + 29 => LOG_THREADS * 256 * RX_BYTES, + 31 => 512 * RX_BYTES, + 34 => 0x40, + 35 => 0x3fc0, + 36 => 0xc8, + 37 => 0x4000, + 38 => RUNTIME_SIZE, + 39 => 0x2658, + 40 => 0x88, + 41 => LOG_THREADS * 256 * LOG_PAYLOAD_BYTES, + 42 => 0xc18, + 43 => 0x1248, + 44 => 0xe10, + 45 => 0xe48, + 46 => 0x670, + 47 => 0xeaf0, + 48 => 0x20, + DYNAMIC_STATE | RETENTION_STATE | GLOBAL_POOL | GLOBAL_PARAMETERS => 0x4000, + PERFORMANCE_CONTROL => 0x6578, + RECOVERY_ACTIVITY => 0x60, + SENSOR_VALUE => 4, + _ => return Err(Error::Bounds), + }; + // Separate page-aligned owners, with room for the largest receive allocation. + // This namespace is disjoint from the existing fixed work and timestamp arenas. + Ok(Allocation { + address: 0xffff_fc21_0000_0000 + (index as u64) * 0x10_0000, + size, + gpu_shared: matches!(index, GLOBAL_POOL | GLOBAL_PARAMETERS), + initial: Initial::Zero, + }) +} + +pub(crate) fn cached(index: usize) -> bool { + matches!(index, 39 | PERFORMANCE_CONTROL) +} + +pub(crate) struct PointerGraph<'a> { + pub(crate) owners: &'a [Region; COUNT], + pub(crate) scratch: Region, + pub(crate) pool_alias: u64, + pub(crate) parameter_alias: u64, +} + +impl PointerGraph<'_> { + /// Check the complete graph, including padded mapping ownership, before encoding it. + fn addresses(&self) -> Result<[u64; COUNT], Error> { + let mut addresses = [0; COUNT]; + for (index, owner) in self.owners.iter().enumerate() { + let size = allocation(index)?.size; + addresses[index] = owner.at(0, size)?; + if addresses[index] & 0x3fff != 0 + || addresses[index] < 0xffff_fc20_0000_0000 + || addresses[index] >= 0xffff_fc2f_fc00_0000 + { + return Err(Error::Address); + } + let mapped = (size + 0x3fff) & !0x3fff; + let end = addresses[index] + .checked_add(mapped as u64) + .ok_or(Error::Address)?; + if end > 0xffff_fc2f_fc00_0000 { + return Err(Error::Address); + } + for prior in 0..index { + let prior_size = (allocation(prior)?.size + 0x3fff) & !0x3fff; + if addresses[index] < addresses[prior] + prior_size as u64 + && addresses[prior] < addresses[index] + mapped as u64 + { + return Err(Error::Address); + } + } + } + let scratch = self.scratch.at(0, 0x8000)?; + let scratch_end = scratch.checked_add(0x8000).ok_or(Error::Address)?; + if scratch & 0x3fff != 0 + || scratch < 0xffff_fc20_0000_0000 + || scratch_end > 0xffff_fc2f_fc00_0000 + { + return Err(Error::Address); + } + for (index, address) in addresses.iter().enumerate() { + let bytes = (allocation(index)?.size + 0x3fff) & !0x3fff; + if scratch < *address + bytes as u64 && *address < scratch_end { + return Err(Error::Address); + } + } + for alias in [self.pool_alias, self.parameter_alias] { + if alias & 0x3fff != 0 || !(0x70_0000_0000..0x70_0200_0000).contains(&alias) { + return Err(Error::Address); + } + } + if self.pool_alias == self.parameter_alias { + return Err(Error::Address); + } + Ok(addresses) + } + + pub(crate) fn root(&self, output: &mut [u8]) -> Result<(), Error> { + self.addresses()?; + Root { + region_a: self.owners[storage::REGION_A], + runtime: self.owners[storage::RUNTIME_POINTERS], + globals: self.owners[storage::GLOBALS], + new_a: self.owners[storage::RUNTIME_FLAGS], + control: self.owners[storage::CONTROL_REGION], + power: self.owners[storage::GLOBALS_POWER], + } + .encode_26(output, self.owners[DYNAMIC_STATE]) + } + + pub(crate) fn runtime(&self, output: &mut [u8]) -> Result<(), Error> { + let addresses = self.addresses()?; + let scratch = self.scratch.at(0, 0x8000)?; + let bytes = output.get_mut(..RUNTIME_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + let pointer = |bytes: &mut [u8], offset: usize, value: u64| { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + }; + pointer(bytes, 0x18, scratch); + pointer(bytes, 0x2d0, self.parameter_alias); + pointer(bytes, 0x2e0, self.pool_alias); + bytes[0x250..0x254].copy_from_slice(&1u32.to_le_bytes()); + bytes[0x2f8..0x2fc].copy_from_slice(&4u32.to_le_bytes()); + for (offset, owner) in [ + (0, storage::HARDWARE_DATA), + (0x10, storage::UNKNOWN_PAIR), + (0x1c0, 26), + (0x1c8, 27), + (0x1d0, 28), + (0x1d8, 29), + (0x1e0, 30), + (0x1e8, 31), + (0x1f0, 32), + (0x1f8, 33), + (0x200, storage::FWLOG_PAYLOAD), + (0x254, storage::UNKNOWN_C0), + (0x25c, storage::UNKNOWN_C1), + (0x264, storage::UNKNOWN_C3), + (0x26c, RECOVERY_ACTIVITY), + (0x2d8, GLOBAL_PARAMETERS), + (0x2e8, GLOBAL_POOL), + (0x469, PERFORMANCE_CONTROL), + ] { + pointer(bytes, offset, addresses[owner]); + } + for channel in 0..13 { + let offset = 0x20 + channel * 0x20; + let state = addresses[channel * 2]; + pointer(bytes, offset, state); + pointer(bytes, offset + 8, state + 0x10); + pointer(bytes, offset + 16, state + 0x20); + pointer(bytes, offset + 24, addresses[channel * 2 + 1]); + } + Ok(()) + } + + pub(crate) fn firmware_control(&self, output: &mut [u8]) -> Result<(), Error> { + let addresses = self.addresses()?; + let bytes = output + .get_mut(..allocation(storage::CONTROL_REGION)?.size) + .ok_or(Error::Size)?; + bytes.fill(0); + bytes[0x4f88..0x4f90].copy_from_slice(&addresses[storage::FW_CONTROL_STATE].to_le_bytes()); + bytes[0x4f90..0x4f98].copy_from_slice(&addresses[storage::FW_CONTROL_RING].to_le_bytes()); + bytes[0x4fd4..0x4fd8].copy_from_slice(&1u32.to_le_bytes()); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn owners(delta: u64) -> [Region; COUNT] { + core::array::from_fn(|index| { + let item = allocation(index).unwrap(); + Region::new(item.address + delta, item.size).unwrap() + }) + } + + fn graph(owners: &[Region; COUNT]) -> PointerGraph<'_> { + PointerGraph { + owners, + scratch: Region::new(0xffff_fc22_0000_0000, 0x8000).unwrap(), + pool_alias: 0x70_0000_0000, + parameter_alias: 0x70_0000_8000, + } + } + + #[test] + fn complete_plan_has_disjoint_page_owners_and_nine_log_threads() { + let owners = owners(0); + assert!(graph(&owners).addresses().is_ok()); + assert_eq!(allocation(28).unwrap().size, 0x1b0); + assert_eq!(allocation(29).unwrap().size, 0x28800); + assert_eq!(allocation(41).unwrap().size, 0x79800); + assert!(allocation(COUNT).is_err()); + } + + #[test] + fn runtime_references_relocate_and_keep_the_packed_power_pointer() { + for delta in [0, 0x1000_0000] { + let owners = owners(delta); + let mut output = [0xa5; RUNTIME_SIZE + 8]; + graph(&owners).runtime(&mut output).unwrap(); + let read = |at| u64::from_le_bytes(output[at..at + 8].try_into().unwrap()); + assert_eq!( + read(0x469), + allocation(PERFORMANCE_CONTROL).unwrap().address + delta + ); + assert_eq!(read(0x1a0), allocation(24).unwrap().address + delta); + assert_eq!(read(0x1b8), allocation(25).unwrap().address + delta); + assert_eq!(read(0x2d0), 0x70_0000_8000); + assert_eq!(&output[RUNTIME_SIZE..], &[0xa5; 8]); + } + } + + #[test] + fn every_short_runtime_rejects_before_mutation() { + let owners = owners(0); + for length in 0..RUNTIME_SIZE { + let mut output = [0xa5; RUNTIME_SIZE]; + assert_eq!( + graph(&owners).runtime(&mut output[..length]), + Err(Error::Size) + ); + assert_eq!(output, [0xa5; RUNTIME_SIZE]); + } + } + + #[test] + fn every_owner_is_checked_before_any_record_is_written() { + for index in 0..COUNT { + let mut owners = owners(0); + let item = allocation(index).unwrap(); + owners[index] = Region::new(item.address, item.size - 1).unwrap(); + let mut output = [0xa5; RUNTIME_SIZE]; + assert!(graph(&owners).runtime(&mut output).is_err()); + assert_eq!(output, [0xa5; RUNTIME_SIZE]); + } + let mut owners = owners(0); + owners[1] = owners[0]; + let mut output = [0xa5; RUNTIME_SIZE]; + assert!(graph(&owners).runtime(&mut output).is_err()); + assert_eq!(output, [0xa5; RUNTIME_SIZE]); + } + #[test] + fn only_atomic_controller_owners_are_firmware_cacheable() { + let selected: Vec<_> = (0..COUNT).filter(|index| cached(*index)).collect(); + assert_eq!(selected, vec![39, PERFORMANCE_CONTROL]); + } + + #[test] + fn scratch_requires_its_firmware_host_window() { + let owners = owners(0); + let mut value = graph(&owners); + value.scratch = Region::new(0xffff_fc10_0000_0000, 0x8000).unwrap(); + let mut output = [0xa5; RUNTIME_SIZE]; + assert_eq!(value.runtime(&mut output), Err(Error::Address)); + assert_eq!(output, [0xa5; RUNTIME_SIZE]); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_init_layout.rs b/drivers/gpu/drm/asahi/m3/m3_init_layout.rs index 92cc818e030ccb..75f06abe6f4110 100644 --- a/drivers/gpu/drm/asahi/m3/m3_init_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_init_layout.rs @@ -78,13 +78,38 @@ impl Root { (root::CONTROL, self.control.at(0, CONTROL_SIZE)?), (root::POWER, self.power.at(0, 2292)?), ]; - let bytes = out.get_mut(..ROOT_SIZE).ok_or(Error::Size)?; + Self::write_root(out, ROOT_SIZE, 0x0c08_e21e_8380_0490, &references) + } + + /// Encode the extended root without admitting a firmware image or publishing it. + pub(crate) fn encode_26(&self, out: &mut [u8], extra: Region) -> Result<(), Error> { + let references = [ + (root::REGION_A, self.region_a.at(0, 0x4000)?), + (root::RUNTIME, self.runtime.at(0, 0x4b8)?), + (root::GLOBALS, self.globals.at(0, 0xe48)?), + (root::NEW_A, self.new_a.at(0, 0x20)?), + (root::CONTROL, self.control.at(0, 0xeaf0)?), + (root::POWER, self.power.at(0, 0x670)?), + (0xc0, extra.at(0, 0x4000)?), + ]; + if references.iter().any(|(_, address)| address & 7 != 0) { + return Err(Error::Address); + } + Self::write_root(out, 0xc8, 0x0c89_c357_8392_04b8, &references) + } + + fn write_root( + out: &mut [u8], + size: usize, + version: u64, + references: &[(usize, u64)], + ) -> Result<(), Error> { + let bytes = out.get_mut(..size).ok_or(Error::Size)?; bytes.fill(0); - // Opaque qualified G15/V14_8_3 version tuple, not a semantic version. - for (i, value) in [0x490u16, 0x8380, 0xe21e, 0xc08].iter().enumerate() { - bytes[root::VERSION + i * 2..root::VERSION + i * 2 + 2].copy_from_slice(&value.to_le_bytes()); + pointer(bytes, root::VERSION, version); + for &(offset, value) in references { + pointer(bytes, offset, value); } - for (offset, value) in references { pointer(bytes, offset, value); } word(bytes, root::HOST_ALLOCATIONS, 1); bytes[root::PAGE_SIZE..root::PAGE_SIZE + 2].copy_from_slice(&0x4000u16.to_le_bytes()); bytes[root::PAGE_BITS] = 14; @@ -193,3 +218,107 @@ impl ControlRegion { Ok(()) } } + +#[cfg(test)] +mod root_format_tests { + use super::*; + + fn allocations() -> (Root, Region) { + let region = + |index: u64| Region::new(0xffff_fc01_0000_0000 + index * 0x10000, 0x10000).unwrap(); + ( + Root { + region_a: region(1), + runtime: region(2), + globals: region(3), + new_a: region(4), + control: region(5), + power: region(6), + }, + region(7), + ) + } + + #[test] + fn root_revision_changes_only_the_version_and_extension() { + let (root, extra) = allocations(); + let mut older = [0x55; 0xd0]; + let mut newer = older; + root.encode(&mut older).unwrap(); + root.encode_26(&mut newer, extra).unwrap(); + assert_eq!(&older[8..0xc0], &newer[8..0xc0]); + assert_eq!( + u64::from_le_bytes(newer[..8].try_into().unwrap()), + 0x0c89_c357_8392_04b8 + ); + assert_eq!( + u64::from_le_bytes(newer[0xc0..0xc8].try_into().unwrap()), + 0xffff_fc01_0007_0000 + ); + assert_eq!(&older[0xc0..], &[0x55; 16]); + assert_eq!(&newer[0xc8..], &[0x55; 8]); + for (offset, index) in [ + (8, 1), + (0x18, 2), + (0x20, 3), + (0xa8, 4), + (0xb0, 5), + (0xb8, 6), + ] { + assert_eq!( + u64::from_le_bytes(newer[offset..offset + 8].try_into().unwrap()), + 0xffff_fc01_0000_0000 + index * 0x10000 + ); + } + } + + #[test] + fn every_short_destination_is_unchanged() { + let (root, extra) = allocations(); + for length in 0..0xc8 { + let mut output = [0xa5; 0xc8]; + assert_eq!( + root.encode_26(&mut output[..length], extra), + Err(Error::Size) + ); + assert_eq!(output, [0xa5; 0xc8]); + } + } + + #[test] + fn every_reference_requires_its_own_complete_allocation() { + for (index, required) in [0x4000, 0x4b8, 0xe48, 0x20, 0xeaf0, 0x670, 0x4000] + .into_iter() + .enumerate() + { + for (size, aligned, valid) in [ + (required - 1, true, false), + (required, false, false), + (required, true, true), + ] { + let (mut root, mut extra) = allocations(); + let reference = match index { + 0 => &mut root.region_a, + 1 => &mut root.runtime, + 2 => &mut root.globals, + 3 => &mut root.new_a, + 4 => &mut root.control, + 5 => &mut root.power, + _ => &mut extra, + }; + *reference = + Region::new(0xffff_fc02_0000_0000 + u64::from(!aligned), size).unwrap(); + let mut output = [0xa5; 0xc8]; + let result = root.encode_26(&mut output, extra); + assert_eq!( + result.is_ok(), + valid, + "reference {index}, size {size}, aligned {aligned}" + ); + if !valid { + assert_eq!(output, [0xa5; 0xc8]); + } + } + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_mmio_26.rs b/drivers/gpu/drm/asahi/m3/m3_mmio_26.rs new file mode 100644 index 00000000000000..a17bc0ee399c34 --- /dev/null +++ b/drivers/gpu/drm/asahi/m3/m3_mmio_26.rs @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! T8122 firmware MMIO descriptors. Mapping ownership stays with the M3 runtime. + +use super::m3_init_layout::{Error, Region}; + +pub(crate) const SLOTS: usize = 53; +pub(crate) const RECORD_BYTES: usize = 40; +pub(crate) const TABLE_OFFSET: usize = 0x640; + +#[derive(Clone, Copy, Debug)] +pub(crate) struct Area { + pub(crate) physical: u64, + pub(crate) bytes: usize, + relative_register: u64, + writable: bool, + split: bool, +} + +impl Area { + pub(crate) fn writable(self) -> bool { + self.writable + } + + pub(crate) fn page_offset(self) -> usize { + (self.physical & 0x3fff) as usize + } + + pub(crate) fn mapped_bytes(self) -> usize { + (self.page_offset() + self.bytes + 0x3fff) & !0x3fff + } + + /// At most two physical banks; the second bank occupies the next virtual pages. + pub(crate) fn banks(self) -> [(u64, usize, usize); 2] { + if self.split { + [ + (self.physical, self.bytes / 2, 0), + (self.physical + 0x200_0000, self.bytes / 2, self.bytes / 2), + ] + } else { + [(self.physical & !0x3fff, self.mapped_bytes(), 0), (0, 0, 0)] + } + } +} + +/// Register resources obtained from the admitted T8122 hardware configuration. +/// Their physical addresses are supplied by the owner, not duplicated in a wire table. +#[derive(Clone, Copy)] +pub(crate) struct RegisterBases { + pub(crate) gpu: u64, + pub(crate) interrupt: u64, + pub(crate) memory: u64, + pub(crate) telemetry_write: u64, + pub(crate) telemetry_read: u64, + pub(crate) neural: u64, + pub(crate) metrology: u64, +} + +/// Select the wire slot and map its logical hardware block independently of its VA. +pub(crate) fn area(slot: usize, bases: RegisterBases) -> Result, Error> { + let (base, offset, bytes, tagged, writable, split) = match slot { + 0 => (bases.interrupt, 0, 0x4000, false, true, false), + 3 => (bases.memory, 0, 0x30000, false, true, true), + 9 => (bases.telemetry_write, 0, 0x1000, false, true, false), + 10 => (bases.telemetry_read, 0, 0x2000, false, false, false), + 12 => (bases.neural, 0, 0x4000, false, true, false), + 14 => (bases.metrology, 0, 0x8000, false, false, false), + 17 => (bases.gpu, 0, 0x20000, false, true, false), + 26 => (bases.gpu, 0xd04000, 0x8000, true, true, false), + 27 => (bases.gpu, 0xd0d000, 0x1000, true, true, false), + 28 => (bases.gpu, 0xd50000, 0x8000, true, true, false), + 29 => (bases.gpu, 0xd10000, 0x4000, true, true, false), + 31 => (bases.gpu, 0xd40000, 0x4000, true, true, false), + 32 => (bases.gpu, 0xd60000, 0x8000, true, true, false), + 35 => (bases.gpu, 0xe00000, 0x4000, true, true, false), + 39 => (bases.gpu, 0xe08000, 0x8000, false, true, false), + 40 => (bases.gpu, 0xe1c000, 0x4000, true, true, false), + 0..SLOTS => return Ok(None), + _ => return Err(Error::Bounds), + }; + let physical = base.checked_add(offset).ok_or(Error::Address)?; + if physical == 0 || physical.checked_add(bytes as u64).ok_or(Error::Address)? > 1 << 42 { + return Err(Error::Address); + } + if split + && (physical & 0x3fff != 0 + || physical + .checked_add(0x200_0000) + .and_then(|v| v.checked_add((bytes / 2) as u64)) + .filter(|v| *v <= 1 << 42) + .is_none()) + { + return Err(Error::Address); + } + Ok(Some(Area { + physical, + bytes, + relative_register: if tagged { offset } else { 0 }, + writable, + split, + })) +} + +fn empty_mode(slot: usize) -> u64 { + if matches!(slot, 2 | 5..=8 | 30 | 33 | 34 | 37 | 38 | 41..=43 | 46..=49 | 52) { + 2 + } else { + 0 + } +} + +/// Encode a complete record after checking the owned mapping; errors preserve all output. +pub(crate) fn encode( + slot: usize, + bases: RegisterBases, + mapping: Option, + output: &mut [u8], +) -> Result<(), Error> { + let descriptor = area(slot, bases)?; + let (physical, va, bytes, requested, register, mode) = match (descriptor, mapping) { + (None, None) => (0, 0, 0, 0, 0, empty_mode(slot)), + (Some(item), Some(mapping)) => { + let page = mapping.at(0, item.mapped_bytes())?; + if page & 0x3fff != 0 { + return Err(Error::Address); + } + let va = page + .checked_add(item.page_offset() as u64) + .ok_or(Error::Address)?; + ( + item.physical, + va, + item.bytes as u32, + if item.split { + (item.bytes / 2) as u32 + } else { + item.bytes as u32 + }, + item.relative_register, + if item.writable { 2 } else { 0 }, + ) + } + _ => return Err(Error::Address), + }; + let bytes_out = output.get_mut(..RECORD_BYTES).ok_or(Error::Size)?; + bytes_out.fill(0); + bytes_out[0..8].copy_from_slice(&physical.to_le_bytes()); + bytes_out[8..16].copy_from_slice(&va.to_le_bytes()); + bytes_out[16..20].copy_from_slice(&bytes.to_le_bytes()); + bytes_out[20..24].copy_from_slice(&requested.to_le_bytes()); + bytes_out[24..32].copy_from_slice(®ister.to_le_bytes()); + bytes_out[32..40].copy_from_slice(&mode.to_le_bytes()); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + // Resource owners exercise subpage mappings and split memory-controller banks. + const BASES: RegisterBases = RegisterBases { + gpu: 0x2_9000_0000, + interrupt: 0x2_d101_4000, + memory: 0x2_201c_4000, + telemetry_write: 0x2_d03d_0000, + telemetry_read: 0x2_d03c_0000, + neural: 0x3_1145_c000, + metrology: 0x2_d028_0000, + }; + + #[test] + fn j613_slot28_bank_and_wire_tag_agree() { + let bank = area(28, BASES).unwrap().unwrap(); + assert_eq!(bank.physical, BASES.gpu + 0xd50000); + assert_eq!(bank.bytes, 0x8000); + let mapped = Region::new(0xffff_fc23_01c0_0000, 0x8000).unwrap(); + let mut bytes = [0; RECORD_BYTES]; + encode(28, BASES, Some(mapped), &mut bytes).unwrap(); + assert_eq!(&bytes[0..8], &(BASES.gpu + 0xd50000).to_le_bytes()); + assert_eq!(&bytes[24..32], &0xd50000u64.to_le_bytes()); + assert_ne!(bank.physical, BASES.gpu + 0xd58000); + } + + #[test] + fn descriptor_inventory_ends_before_the_retention_pointer() { + assert_eq!(TABLE_OFFSET + SLOTS * RECORD_BYTES, 0xe88); + assert_eq!( + (0..SLOTS) + .filter(|slot| area(*slot, BASES).unwrap().is_some()) + .count(), + 16 + ); + assert!(area(SLOTS, BASES).is_err()); + } + + #[test] + fn discontiguous_memory_controller_banks_have_distinct_physical_owners() { + let item = area(3, BASES).unwrap().unwrap(); + assert_eq!( + item.banks(), + [ + (0x2_201c_4000, 0x18000, 0), + (0x2_221c_4000, 0x18000, 0x18000) + ] + ); + let mapping = Region::new(0xffff_fc23_0000_0000, 0x30000).unwrap(); + let mut bytes = [0xa5; RECORD_BYTES]; + encode(3, BASES, Some(mapping), &mut bytes).unwrap(); + assert_eq!( + u32::from_le_bytes(bytes[16..20].try_into().unwrap()), + 0x30000 + ); + assert_eq!( + u32::from_le_bytes(bytes[20..24].try_into().unwrap()), + 0x18000 + ); + } + + #[test] + fn subpage_mapping_keeps_the_register_offset() { + let item = area(27, BASES).unwrap().unwrap(); + assert_eq!(item.page_offset(), 0x1000); + assert_eq!(item.banks()[0], (0x2_90d0_c000, 0x4000, 0)); + let mut output = [0xa5; RECORD_BYTES + 8]; + encode( + 27, + BASES, + Some(Region::new(0xffff_fc23_0000_0000, 0x4000).unwrap()), + &mut output, + ) + .unwrap(); + assert_eq!( + u64::from_le_bytes(output[8..16].try_into().unwrap()), + 0xffff_fc23_0000_1000 + ); + assert_eq!(&output[RECORD_BYTES..], &[0xa5; 8]); + } + + #[test] + fn empty_slots_keep_modes_without_fabricating_mappings() { + for slot in 0..SLOTS { + if area(slot, BASES).unwrap().is_some() { + continue; + } + let mut output = [0xa5; RECORD_BYTES]; + encode(slot, BASES, None, &mut output).unwrap(); + assert_eq!(&output[..32], &[0; 32]); + assert_eq!( + u64::from_le_bytes(output[32..40].try_into().unwrap()), + empty_mode(slot) + ); + } + } + + #[test] + fn invalid_mapping_or_output_never_partially_changes_a_descriptor() { + let good = Region::new(0xffff_fc23_0000_0000, 0x4000).unwrap(); + for (slot, mapping) in [ + (SLOTS, None), + (0, None), + (1, Some(good)), + (0, Some(Region::new(0xffff_fc23_0000_0001, 0x4000).unwrap())), + (0, Some(Region::new(0xffff_fc23_0000_0000, 0x3fff).unwrap())), + ] { + let mut output = [0xa5; RECORD_BYTES]; + assert!(encode(slot, BASES, mapping, &mut output).is_err()); + assert_eq!(output, [0xa5; RECORD_BYTES]); + } + for length in 0..RECORD_BYTES { + let mut output = [0xa5; RECORD_BYTES]; + assert_eq!( + encode(0, BASES, Some(good), &mut output[..length]), + Err(Error::Size) + ); + assert_eq!(output, [0xa5; RECORD_BYTES]); + } + } + #[test] + fn split_banks_require_page_alignment_and_both_fit_the_bus() { + for memory in [BASES.memory + 4, (1 << 42) - 0x100_0000] { + let mut bases = BASES; + bases.memory = memory; + let mut output = [0xa5; RECORD_BYTES]; + assert!(encode( + 3, + bases, + Some(Region::new(0xffff_fc21_0000_0000, 0x30000).unwrap()), + &mut output + ) + .is_err()); + assert_eq!(output, [0xa5; RECORD_BYTES]); + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_parameter_layout.rs b/drivers/gpu/drm/asahi/m3/m3_parameter_layout.rs index 684505615adf3b..97c1367b29455d 100644 --- a/drivers/gpu/drm/asahi/m3/m3_parameter_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_parameter_layout.rs @@ -61,3 +61,77 @@ impl Manager { Ok(()) } } + +impl Manager { + pub(crate) const EXTENDED_SIZE: usize = 0xc0; + + /// J613's extended manager has both firmware and GPU page-list aliases. + /// The block ring is read through its canonical firmware mapping. + pub(crate) fn encode_extended(&self, pages_fw: FirmwareVa, blocks_fw: FirmwareVa, + hardware_id: u32, output: &mut [u8]) -> Result<(), Error> { + let capacity = self.block_count.checked_add(1).and_then(u32::checked_next_power_of_two) + .ok_or(Error::Size)?; + if hardware_id >= 32 || capacity as usize > self.blocks.size() / 8 { + return Err(Error::Size); + } + let bytes = output.get_mut(..Self::EXTENDED_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + let word = |bytes: &mut [u8], offset: usize, value: u32| { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + }; + let address = |bytes: &mut [u8], offset: usize, value: u64| { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + }; + word(bytes, 8, hardware_id); + word(bytes, 12, hardware_id); + address(bytes, 0x1c, pages_fw.get()); + address(bytes, 0x24, self.pages.base()); + word(bytes, 0x2c, self.pages.size() as u32); + word(bytes, 0x30, self.page_count); + word(bytes, 0x34, capacity); + word(bytes, 0x38, self.block_count); + address(bytes, 0x40, blocks_fw.get()); + address(bytes, 0x48, self.control.get()); + word(bytes, 0x50, self.page_count - 1); + word(bytes, 0x54, BLOCK_SIZE as u32); + address(bytes, 0x60, self.counter.get()); + word(bytes, 0x78, self.page_count); + word(bytes, 0x7c, self.page_count); + Ok(()) + } +} + +#[cfg(test)] +mod extended_tests { + use super::*; + fn fixture() -> Manager { + Manager::new(GpuRegion::new(0x5000_0000, 0x4000).unwrap(), + GpuRegion::new(0x6000_0000, 0x4000).unwrap(), + FirmwareVa::new(0xffff_fc24_0010_0000).unwrap(), + FirmwareVa::new(0xffff_fc24_0020_0000).unwrap(), 32).unwrap() + } + #[test] fn native_manager_keeps_distinct_aliases_and_hardware_identity() { + let mut bytes = [0xa5; Manager::EXTENDED_SIZE + 8]; + let pages = FirmwareVa::new(0xffff_fc24_0030_0000).unwrap(); + let blocks = FirmwareVa::new(0xffff_fc24_0040_0000).unwrap(); + fixture().encode_extended(pages, blocks, 7, &mut bytes).unwrap(); + let word = |at| u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()); + let pointer = |at| u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap()); + assert_eq!(word(8), 7); assert_eq!(word(12), 7); + assert_eq!(pointer(0x1c), pages.get()); + assert_eq!(pointer(0x24), 0x5000_0000); + assert_eq!(pointer(0x40), blocks.get()); + assert_eq!(word(0x34), 64); assert_eq!(word(0x38), 32); + assert_eq!(word(0x50), 127); + assert_eq!(pointer(0x80), 0); + assert_eq!(&bytes[Manager::EXTENDED_SIZE..], &[0xa5; 8]); + } + #[test] fn native_manager_rejects_short_storage_and_invalid_identity_before_mutation() { + let fw = FirmwareVa::new(0xffff_fc24_0010_0000).unwrap(); + let mut bytes = [0xa5; Manager::EXTENDED_SIZE]; + assert!(fixture().encode_extended(fw, fw, 32, &mut bytes).is_err()); + assert_eq!(bytes, [0xa5; Manager::EXTENDED_SIZE]); + assert!(fixture().encode_extended(fw, fw, 0, &mut bytes[..MANAGER_SIZE]).is_err()); + assert_eq!(bytes, [0xa5; Manager::EXTENDED_SIZE]); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_pass.rs b/drivers/gpu/drm/asahi/m3/m3_pass.rs index 94c2172e33279b..9c0eb070039cdd 100644 --- a/drivers/gpu/drm/asahi/m3/m3_pass.rs +++ b/drivers/gpu/drm/asahi/m3/m3_pass.rs @@ -8,10 +8,10 @@ use layout::{Field,Space}; pub(crate) struct Pass {buffers:KVec} impl Pass { pub(crate) fn new(dev:&driver::AsahiDrmDevice,uat:&mmu::Uat,vm:&mmu::Vm, - slot:usize,clusters:u32,aliases:&mut KVec)->Result { + slot:usize,clusters:u32,profile:super::m3_profile::Profile,aliases:&mut KVec)->Result { let mut buffers=KVec::with_capacity(layout::COUNT,GFP_KERNEL)?; for field in layout::FIELDS { - let a=layout::board_allocation(slot,field,clusters).map_err(|_|EINVAL)?; + let a=layout::allocation_for(profile,slot,field,clusters).map_err(|_|EINVAL)?; let mut b=match a.space { Space::ClientGpu=>Buffer::at_prot(dev,uat.kernel_vm(),Some((vm,a.address)),None, a.size,prot::PROT_GPU_FW_SHARED_RW,prot::Prot::from_pte(a.access.pte()))?, diff --git a/drivers/gpu/drm/asahi/m3/m3_pass_layout.rs b/drivers/gpu/drm/asahi/m3/m3_pass_layout.rs index a8a2e23c0fbd14..2ec8f8eb212e57 100644 --- a/drivers/gpu/drm/asahi/m3/m3_pass_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_pass_layout.rs @@ -215,3 +215,39 @@ mod tests { } } } + +pub(crate) fn allocation_for(profile: super::m3_profile::Profile, slot:usize, field:Field, clusters:u32) + -> Result { + let mut owner = board_allocation(slot,field,clusters)?; + if profile == super::m3_profile::Profile::Legacy { return Ok(owner); } + if owner.space == Space::Firmware { + owner.address = 0xffff_fc25_0000_0000 + (slot * COUNT + field as usize) as u64 * 0x10_0000; + } + match field { + Field::Scene => owner.size = 0x400, + Field::TilerCommand | Field::FragmentCommand => { + let fragment = field == Field::FragmentCommand; + owner.size = if fragment {0xca0} else {0x940}; + owner.gpu_alias = Some(0x72_1000_0000 + slot as u64 * 0x4_0000 + + u64::from(fragment) * 0x4000); + } + Field::TilerToFragment | Field::TilerDependency | Field::FragmentDependency => owner.size = 0x40, + _ => {} + } + Ok(owner) +} + +#[cfg(test)] +mod extended_tests { + use super::*; + #[test] fn extended_command_aliases_keep_whole_disjoint_pages() { + for slot in 0..SLOTS { + for field in [Field::TilerCommand,Field::FragmentCommand] { + let owner = allocation_for(super::super::m3_profile::Profile::Extended,slot,field,1).unwrap(); + assert_eq!(owner.address & 0x3fff,0); + assert_eq!(owner.gpu_alias.unwrap() & 0x3fff,0); + assert!(owner.size <= 0x4000); + } + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_pool.rs b/drivers/gpu/drm/asahi/m3/m3_pool.rs index ab50ab7eca8a17..4eb284e1291bdb 100644 --- a/drivers/gpu/drm/asahi/m3/m3_pool.rs +++ b/drivers/gpu/drm/asahi/m3/m3_pool.rs @@ -39,3 +39,34 @@ impl Owners { objects[self.manager].write(0, &manager) } } + +impl Owners { + pub(crate) fn initialize_extended(&self, objects: &mut [Buffer], slot: u8, + compute_only: bool) -> Result { + let mut backings = KVec::new(); + for index in self.backing.clone().take(8) { + let buffer = objects.get(index).ok_or(EINVAL)?; + backings.push(wire::GpuRegion::new(buffer.gpu_va()?, buffer.size()).map_err(|_| EINVAL)?, GFP_KERNEL)?; + } + let backing = wire::ModernBacking(&backings); + let populated = backing.pages().map_err(|_| EINVAL)?; + let pages = objects.get(self.pages).ok_or(EINVAL)?; + let entries = wire::GpuRegion::new(pages.gpu_va()?, pages.size()).map_err(|_| EINVAL)?; + let table = objects.get(self.table).ok_or(EINVAL)?; + let groups = wire::GpuRegion::new(table.gpu_va()?, table.size()).map_err(|_| EINVAL)?; + if groups.size() < populated / 512 * 64 { return Err(EINVAL); } + let descriptor = wire::ModernPool { entries, groups, + submitted: FirmwareVa::new(objects.get(self.counter).ok_or(EINVAL)?.va()).map_err(|_| EINVAL)?, + identity: FirmwareVa::new(objects.get(self.manager).ok_or(EINVAL)?.va()).map_err(|_| EINVAL)?, + slot, capacity: (entries.size() / 8).try_into()?, populated: populated.try_into()?, compute_only }; + let mut bytes = [0; wire::ModernPool::SIZE]; + descriptor.encode(&mut bytes).map_err(|_| EINVAL)?; + for index in 0..populated { + objects[self.pages].u64(index * 8, backing.page(index).map_err(|_| EINVAL)?)?; + } + for index in 0..populated / 512 { + objects[self.table].write(index * 64, &backing.group(index).map_err(|_| EINVAL)?)?; + } + objects[self.manager].write(0, &bytes) + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_pool_layout.rs b/drivers/gpu/drm/asahi/m3/m3_pool_layout.rs index a8474d262bc26d..ac7e6771f7880b 100644 --- a/drivers/gpu/drm/asahi/m3/m3_pool_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_pool_layout.rs @@ -69,3 +69,131 @@ impl<'a> Pool<'a> { Ok(()) } } + +/// Extended firmware's private-memory pool descriptor. The owners and initial +/// population are supplied separately from the descriptor's firmware state. +pub(crate) struct ModernPool { + pub(crate) identity: FirmwareVa, + pub(crate) slot: u8, + pub(crate) entries: GpuRegion, + pub(crate) groups: GpuRegion, + pub(crate) submitted: FirmwareVa, + pub(crate) capacity: u32, + pub(crate) populated: u32, + pub(crate) compute_only: bool, +} + +impl ModernPool { + pub(crate) const SIZE: usize = 0xa0; + + pub(crate) fn encode(&self, output: &mut [u8]) -> Result<(), Error> { + let entry_bytes = (self.capacity as usize).checked_mul(8).ok_or(Error::Size)?; + let group_bytes = (self.populated as usize / 512).checked_mul(8).ok_or(Error::Size)?; + if !(1..=64).contains(&self.slot) || self.capacity == 0 || self.populated >= self.capacity || self.populated % 512 != 0 + || self.entries.base() & 0x3fff != 0 || self.groups.base() & 0x3fff != 0 + || self.entries.size() < entry_bytes || self.groups.size() < group_bytes + || self.submitted.get() & 7 != 0 || self.identity.get() & 7 != 0 { return Err(Error::Address); } + let group_count: u32 = group_bytes.try_into().map_err(|_| Error::Size)?; + let bytes = output.get_mut(..Self::SIZE).ok_or(Error::Size)?; + bytes.fill(0); + let word = |bytes: &mut [u8], offset: usize, value: u32| { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + }; + bytes[..8].copy_from_slice(&self.identity.get().to_le_bytes()); + word(bytes, 8, self.slot.into()); + word(bytes, 12, (self.populated == 0).into()); + word(bytes, 16, 2); + bytes[0x14..0x1c].copy_from_slice(&self.entries.base().to_le_bytes()); + word(bytes, 0x1c, self.capacity); + word(bytes, 0x24, self.populated); + word(bytes, 0x28, (self.populated == 0).into()); + word(bytes, 0x2c, self.populated); + bytes[0x30..0x38].copy_from_slice(&self.groups.base().to_le_bytes()); + word(bytes, 0x40, 4); + word(bytes, 0x48, group_count); + bytes[0x4c..0x54].copy_from_slice(&self.submitted.get().to_le_bytes()); + word(bytes, 0x5c, (self.compute_only && self.populated != 0).into()); + word(bytes, 0x84, (self.populated != 0).into()); + word(bytes, 0x94, 2); + Ok(()) + } +} + +#[cfg(test)] +mod modern_tests { + use super::*; + fn pool(compute_only: bool) -> ModernPool { + ModernPool { identity: FirmwareVa::new(0xffff_fc21_9100_0000).unwrap(), slot: 1, + entries: GpuRegion::new(0x5000_0000, 0x20000).unwrap(), + groups: GpuRegion::new(0x6000_0000, 0x4000).unwrap(), + submitted: FirmwareVa::new(0xffff_fc21_9000_0000).unwrap(), + capacity: 0x4000, populated: 512, compute_only } + } + fn word(bytes: &[u8], offset: usize) -> u32 { + u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()) + } + #[test] fn shared_render_pool_never_gets_compute_exclusion() { + for compute in [false, true] { + let mut output = [0xa5; ModernPool::SIZE + 8]; + pool(compute).encode(&mut output).unwrap(); + assert_eq!(word(&output, 0x5c), compute.into()); + assert_eq!(word(&output, 0x48), 8); + assert_eq!(word(&output, 0x84), 1); + assert_eq!(&output[ModernPool::SIZE..], &[0xa5; 8]); + } + } + #[test] fn bad_population_and_insufficient_owners_reject_before_mutation() { + for pages in [1, 511, 513, 0x4000] { + let mut value = pool(true); value.populated = pages; + let mut output = [0xa5; ModernPool::SIZE]; + assert!(value.encode(&mut output).is_err()); + assert_eq!(output, [0xa5; ModernPool::SIZE]); + } + let mut value = pool(true); value.capacity = 0x4001; + let mut output = [0xa5; ModernPool::SIZE]; + assert!(value.encode(&mut output).is_err()); + assert_eq!(output, [0xa5; ModernPool::SIZE]); + } +} + +/// Populate the extended freelist from independently retained 4-MiB owners. +/// Hardware group records describe 2-MiB subregions, not entire owners. +pub(crate) struct ModernBacking<'a>(pub(crate) &'a [GpuRegion]); +impl ModernBacking<'_> { + pub(crate) fn pages(&self) -> Result { + if self.0.is_empty() || self.0.len() > 8 + || self.0.iter().any(|owner| owner.size() != BACKING_SIZE || owner.base() & 0x3fff != 0) { + return Err(Error::Size); + } + Ok(self.0.len() * BACKING_SIZE / PAGE_SIZE) + } + pub(crate) fn page(&self, index: usize) -> Result { + if index >= self.pages()? { return Err(Error::Index); } + let per_owner = BACKING_SIZE / PAGE_SIZE; + Ok(self.0[index / per_owner].base() + (index % per_owner * PAGE_SIZE) as u64) + } + pub(crate) fn group(&self, index: usize) -> Result<[u8; 64], Error> { + if index >= self.pages()? / 512 { return Err(Error::Index); } + let base = self.page(index * 512)?; + let mut record = [0; 64]; + record[..8].copy_from_slice(&(base | (1 << 61)).to_le_bytes()); + Ok(record) + } +} + +#[cfg(test)] +mod backing_tests { + use super::*; + #[test] fn group_records_never_cross_a_noncontiguous_backing_owner() { + let owners = [GpuRegion::new(0x5000_0000, BACKING_SIZE).unwrap(), + GpuRegion::new(0x6000_0000, BACKING_SIZE).unwrap()]; + let backing = ModernBacking(&owners); + assert_eq!(backing.pages(), Ok(2048)); + assert_eq!(backing.page(1024), Ok(0x6000_0000)); + let second = backing.group(2).unwrap(); + assert_eq!(u64::from_le_bytes(second[..8].try_into().unwrap()), (1 << 61) | 0x6000_0000); + assert_eq!(&second[8..], &[0; 56]); + assert!(backing.group(4).is_err()); + assert!(backing.page(2048).is_err()); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_power_26.rs b/drivers/gpu/drm/asahi/m3/m3_power_26.rs new file mode 100644 index 00000000000000..2619c24b4eb6a3 --- /dev/null +++ b/drivers/gpu/drm/asahi/m3/m3_power_26.rs @@ -0,0 +1,414 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Build the extended M3 controller images from admitted device-tree inputs. +//! These are constructor inputs. Firmware owns subsequent controller outputs. + +use super::{hw, initdata::PStateTables}; +use crate::{f32, float::F32}; +use kernel::prelude::*; + +struct Image(KVec); +impl Image { + fn new(size: usize) -> Result { + let mut bytes = KVec::new(); + bytes.resize(size, 0, GFP_KERNEL)?; + Ok(Self(bytes)) + } + fn word(&mut self, offset: usize, value: u32) -> Result { + self.0 + .get_mut(offset..offset.checked_add(4).ok_or(EOVERFLOW)?) + .ok_or(ERANGE)? + .copy_from_slice(&value.to_le_bytes()); + Ok(()) + } + fn pointer(&mut self, offset: usize, value: u64) -> Result { + self.0 + .get_mut(offset..offset.checked_add(8).ok_or(EOVERFLOW)?) + .ok_or(ERANGE)? + .copy_from_slice(&value.to_le_bytes()); + Ok(()) + } + fn float(&mut self, offset: usize, value: F32) -> Result { + let bits = value.to_bits(); + if bits & 0x7f80_0000 == 0x7f80_0000 { + return Err(EINVAL); + } + self.word(offset, bits) + } + fn reciprocal(&mut self, negative: usize, positive: usize, periods: u32) -> Result { + if periods == 0 { + return Err(EINVAL); + } + let alpha = f32!(1.0) / F32::from(periods); + self.float(negative, f32!(1.0) - alpha)?; + self.float(positive, alpha) + } +} + +pub(crate) struct PowerImages { + pub(crate) hardware: KVec, + pub(crate) performance: KVec, + pub(crate) power: KVec, + pub(crate) globals: KVec, + pub(crate) maximum_khz: u32, +} + +impl PowerImages { + /// `sram_uv` follows the same OPP iteration order as PwrConfig::perf_states. + /// No reference-bin voltage or experimental OPP-power estimate is substituted. + pub(crate) fn new( + pwr: &hw::PwrConfig, + sram_uv: &[u32], + sensor: u64, + retention: u64, + timestamp_base: u64, + io_records: &[u8], + ) -> Result { + let table = PStateTables::new(pwr, true, None, false)?; + let second = table.secondary.ok_or(EINVAL)?; + if table.len != 9 + || table.max != 8 + || sram_uv.len() != pwr.perf_states.len() + || pwr.perf_base_pstate == 0 + || pwr.perf_base_pstate > 8 + || !(1..=1000).contains(&pwr.power_sample_period) + || pwr.pwr_min_duty_cycle > 100 + || pwr.avg_power_min_duty_cycle > 100 + || io_records.len() != super::m3_mmio_26::SLOTS * super::m3_mmio_26::RECORD_BYTES + || [sensor, retention, timestamp_base] + .iter() + .any(|value| *value < 0xffff_fc00_0000_0000 || value & 7 != 0) + { + return Err(EINVAL); + } + let top = &pwr.perf_states[table.primary[8] as usize]; + let base = &pwr.perf_states[table.primary[pwr.perf_base_pstate as usize] as usize]; + if top.freq_hz != 1_338_000_000 || base.freq_hz >= top.freq_hz { + return Err(EINVAL); + } + let period = pwr.power_sample_period; + let ppm_periods = pwr.ppm_filter_time_constant_ms / period; + let average_periods = pwr.avg_power_filter_tc_ms / period; + if [ + ppm_periods, + average_periods, + pwr.pwr_filter_time_constant, + pwr.perf_filter_time_constant, + pwr.perf_filter_time_constant2, + pwr.avg_power_target_filter_tc, + pwr.se_filter_time_constant, + pwr.se_filter_time_constant_1, + ] + .contains(&0) + { + return Err(EINVAL); + } + let clocks = |count: u32| { + count + .checked_mul(period) + .and_then(|v| v.checked_mul(24_000)) + .ok_or(EOVERFLOW) + }; + let dt = F32::from(period) / f32!(1000.0); + let ceiling = 800; + let base_units = pwr.perf_base_pstate * 100; + // The profile controller budget is independent of the loader's experimental + // OPP power model. It is not a demand floor or a replacement voltage table. + let budget = 22_044u32; + let mut main = Image::new(0x2658)?; + let mut performance = Image::new(0x6578)?; + let mut power = Image::new(0x670)?; + let mut globals = Image::new(0xe48)?; + + main.pointer(0x28, timestamp_base)?; + main.0[0x640..0xe88].copy_from_slice(io_records); + main.pointer(0xe88, retention)?; + main.word(0xe90, 0x8122)?; + main.word(0xe94, 2)?; + main.word(0xe9c, 4)?; + main.word(0xea0, 1)?; + main.word(0xea4, 1)?; + main.word(0xeb8, 1)?; + main.word(0xec0, 1)?; + main.word(0xec8, 1)?; + main.word(0xed0, 24_000)?; + main.word(0xed4, 1)?; + main.word(0xed8, period)?; + main.word(0xee0, 1)?; + main.word(0xee4, 1)?; + main.word(0xee8, 1)?; + main.word(0xf04, 31)?; + main.pointer(0xf24, 4)?; + main.word(0xf34, 1)?; + // Firmware completion is polled through its normal bounded status path. + main.word(0xf38, 0)?; + main.word(0xf4c, 49)?; + main.word(0xf6c, 1)?; + main.word(0xf88, 10)?; + main.word(0xf8c, 3)?; + main.word(0xfac, 1)?; + main.word(0xfb8, 22)?; + main.word(0xfbc, 5)?; + main.word(0xfc0, 10)?; + main.word(0xfc4, 8)?; + main.word(0x1cd8, 8)?; + for state in 0..9 { + let primary = table.primary[state] as usize; + let secondary = second[state] as usize; + let item = &pwr.perf_states[primary]; + let auxiliary = &pwr.perf_states[secondary]; + if item.volt_mv.len() != 1 + || auxiliary.volt_mv.len() != 1 + || !(100..=1100).contains(&item.volt_mv[0]) + || !(100..=1100).contains(&auxiliary.volt_mv[0]) + || !(100_000..=1_100_000).contains(&sram_uv[primary]) + || !(100_000..=1_100_000).contains(&sram_uv[secondary]) + || sram_uv[primary].div_ceil(1000) < item.volt_mv[0] + || sram_uv[secondary].div_ceil(1000) < auxiliary.volt_mv[0] + { + return Err(EINVAL); + } + main.word(0xfc8 + state * 4, item.freq_hz / 1_000_000)?; + main.word(0x1808 + state * 4, auxiliary.freq_hz / 1_000_000)?; + main.float(0x1848 + state * 4, f32!(1.02))?; + for lane in 0..16 { + main.word(0x1008 + state * 64 + lane * 4, item.volt_mv[0])?; + main.word( + 0x1408 + state * 64 + lane * 4, + sram_uv[primary].div_ceil(1000), + )?; + } + main.word(0x1cdc + state * 4, auxiliary.freq_hz / 1_000_000)?; + if state != 0 { + main.word(0x1d1c + state * 64, auxiliary.volt_mv[0])?; + } + main.word(0x211c + state * 64, sram_uv[secondary].div_ceil(1000))?; + if state != 0 { + // Profile indices for the admitted eight-state ladder. + let core_profile = if state <= 3 { state } else { 2 * state - 3 }; + let auxiliary_profile = if state <= 4 { state } else { 2 * state - 4 }; + main.word(0x19c8 + state * 4, core_profile as u32)?; + main.word(0x1a08 + state * 4, auxiliary_profile as u32)?; + let boost = item.freq_hz.saturating_sub(base.freq_hz) / 1_000_000; + main.word( + 0x1908 + state * 4, + 100 * boost / ((top.freq_hz - base.freq_hz) / 1_000_000), + )?; + } + } + // The native relative-power calibration of each running firmware state. + // Encode only these semantic percentages, not an object-image overlay. + for (state, percent) in [15, 25, 36, 46, 58, 75, 90, 100].into_iter().enumerate() { + main.word(0x18c8 + (state + 1) * 4, percent)?; + } + main.word(0x2544, 4)?; + main.word(0x2548, 6)?; + main.word(0x254c, 3)?; + main.word(0x2550, 7)?; + main.word(0x2554, 7)?; + main.word(0x2568, 10)?; + main.word(0x2570, 1)?; + main.word(0x258c, 0x160000)?; + main.word(0x2594, 1)?; + main.word(0x2598, 1)?; + main.word(0x259c, 1)?; + main.word(0x25a4, 1)?; + main.word(0x25a8, 1)?; + main.word(0x25ac, 1)?; + main.0[0x25b4..0x25e4].fill(0xff); + main.word(0x2604, 1)?; + main.word(0x2634, 1)?; + + globals.word(0x24, 3000)?; + globals.word(0x78, 1)?; + globals.word(0x84, budget)?; + for offset in [0x88, 0x8c, 0x98] { + globals.word(offset, ceiling)?; + } + globals.word(0x998, 40)?; + globals.word(0x99c, 10)?; + globals.word(0x9a0, 250)?; + globals.word(0x9b8, 2)?; + globals.word(0x9bc, 40)?; + globals.word(0x9c0, 5)?; + globals.word(0x9c8, 40)?; + globals.word(0x9cc, 50)?; + globals.word(0xe28, 3)?; + + power.word(0x10, average_periods)?; + power.float(0x14, pwr.avg_power_ki_only * dt)?; + power.float(0x18, pwr.avg_power_kp)?; + power.word(0x1c, pwr.avg_power_min_duty_cycle)?; + power.word(0x20, pwr.avg_power_target_filter_tc)?; + power.word(0xc8, 11_000)?; + power.word(0xcc, 8_000)?; + power.float(0xd8, pwr.fast_die0_proportional_gain)?; + power.float(0xdc, pwr.fast_die0_integral_gain * dt)?; + power.word(0x12c, budget)?; + power.float(0x130, pwr.ppm_kp)?; + power.float(0x134, pwr.ppm_ki * dt)?; + power.word(0x14c, 1)?; + + performance.word(4, pwr.pwr_sample_period_aic_clks)?; + performance.word(8, pwr.pwr_sample_period_aic_clks)?; + performance.word(0x10, 4)?; + performance.float(0x14, f32!(1.0))?; + performance.word(0x2c, 1)?; + performance.word(0x30, 1)?; + // Leave the observed hardware state at zero until firmware writes it. + for offset in [0x40, 0x80, 0xb78, 0xb80, 0xbc0] { + performance.word(offset, base_units)?; + } + for offset in [0x44, 0x6c, 0x70, 0x84] { + performance.word(offset, 1)?; + } + for offset in [ + 0x48, 0x88, 0x9f4, 0x9f8, 0xa38, 0xaac, 0xaf0, 0xb7c, 0x221c, 0x22b4, 0x22b8, 0x22f8, + 0x2870, 0x29f8, 0x29fc, 0x2a3c, 0x2b88, 0x2c44, 0x2c88, + ] { + performance.word(offset, ceiling)?; + } + performance.word(0x50, 100)?; + performance.word(0x60, 4)?; + performance.float(0x64, f32!(1.0))?; + performance.word(0x90, 100)?; + performance.word(0x9b8, 625)?; + performance.reciprocal(0x9c4, 0x9cc, pwr.pwr_filter_time_constant)?; + performance.float(0x9d4, pwr.pwr_integral_gain)?; + performance.float(0x9dc, F32::from(pwr.pwr_integral_min_clamp))?; + performance.float(0x9e0, F32::from(budget))?; + performance.float(0x9e4, pwr.pwr_proportional_gain)?; + performance.float(0x9ec, -F32::from(ceiling) / F32::from(budget))?; + for offset in [0x9f0, 0xaa8, 0x22b0] { + performance.word(offset, pwr.pwr_min_duty_cycle)?; + } + for offset in [0xa04, 0xabc, 0x28c8, 0x28cc, 0x28d0, 0x2a08] { + performance.word(offset, budget)?; + } + performance.word(0xa70, ppm_periods.checked_mul(4).ok_or(EOVERFLOW)?)?; + performance.reciprocal(0xa7c, 0xa84, ppm_periods)?; + performance.float(0xa8c, pwr.ppm_ki * dt)?; + performance.float(0xa98, f32!(65536.0))?; + performance.float(0xa9c, pwr.ppm_kp)?; + performance.word(0xac8, pwr.ppm_filter_time_constant_ms)?; + performance.word(0xad0, clocks(ppm_periods)?)?; + performance.word(0xb28, pwr.perf_tgt_utilization)?; + performance.word(0xb30, pwr.perf_boost_min_util)?; + performance.word(0xb34, pwr.perf_boost_ce_step)?; + performance.word(0xb38, pwr.perf_reset_iters)?; + performance.word(0xb40, 6)?; + performance.word(0xb44, 1)?; + performance.word(0xb48, pwr.perf_filter_drop_threshold)?; + performance.reciprocal(0xb4c, 0xb54, pwr.perf_filter_time_constant)?; + performance.reciprocal(0xb50, 0xb58, pwr.perf_filter_time_constant2)?; + performance.float(0xb5c, pwr.perf_integral_gain)?; + performance.float(0xb60, pwr.perf_integral_gain2)?; + performance.float(0xb64, F32::from(pwr.perf_integral_min_clamp))?; + performance.float(0xb68, f32!(95.0))?; + performance.float(0xb6c, pwr.perf_proportional_gain)?; + performance.float(0xb70, pwr.perf_proportional_gain2)?; + performance.float(0xb74, F32::from(8 - pwr.perf_base_pstate) / f32!(0.95))?; + performance.word(0xb8c, pwr.perf_tgt_utilization)?; + performance.float(0x21f0, f32!(65536.0))?; + performance.float(0x21f4, F32::from(pwr.pwr_min_duty_cycle))?; + performance.float(0x21f8, F32::from(ceiling))?; + performance.word(0x21fc, 1338)?; + performance.float(0x2218, f32!(100.0))?; + performance.float(0x2224, f32!(0.8))?; + performance.float(0x2228, f32!(0.2))?; + performance.word(0x2268, 0x4248)?; + performance.word(0x2270, 8_000)?; + performance.word(0x2278, 4)?; + performance.float(0x228c, f32!(1.0))?; + performance.float(0x2294, pwr.fast_die0_integral_gain * dt)?; + performance.float(0x22a0, f32!(65536.0))?; + performance.float(0x22a4, pwr.fast_die0_proportional_gain)?; + performance.word(0x22c4, 11_000)?; + performance.word(0x28c0, 1)?; + performance.reciprocal(0x28dc, 0x28e0, pwr.avg_power_target_filter_tc)?; + performance.word( + 0x28e4, + pwr.avg_power_target_filter_tc + .checked_mul(4) + .ok_or(EOVERFLOW)?, + )?; + performance.word( + 0x28e8, + pwr.avg_power_target_filter_tc + .checked_mul(period) + .ok_or(EOVERFLOW)?, + )?; + performance.word(0x28ec, clocks(pwr.avg_power_target_filter_tc)?)?; + performance.word(0x29bc, average_periods.checked_mul(4).ok_or(EOVERFLOW)?)?; + performance.reciprocal(0x29c8, 0x29d0, average_periods)?; + performance.float(0x29d8, pwr.avg_power_ki_only * dt)?; + performance.float(0x29e4, f32!(65536.0))?; + performance.float(0x29e8, pwr.avg_power_kp)?; + performance.word(0x29f4, pwr.avg_power_min_duty_cycle)?; + performance.float(0x2a04, F32::from(budget))?; + performance.word(0x2a14, pwr.avg_power_filter_tc_ms)?; + performance.word(0x2a1c, clocks(average_periods)?)?; + performance.word(0x2b4c, 50)?; + performance.word(0x2b50, 1)?; + performance.reciprocal(0x2b58, 0x2b60, pwr.se_filter_time_constant)?; + performance.reciprocal(0x2b5c, 0x2b64, pwr.se_filter_time_constant_1)?; + performance.float(0x2b68, pwr.se_ki * dt)?; + performance.float(0x2b6c, pwr.se_ki_1 * dt)?; + performance.float(0x2b74, f32!(65536.0))?; + performance.float(0x2b78, pwr.se_kp)?; + performance.float(0x2b7c, pwr.se_kp_1)?; + performance.word(0x2b84, 100)?; + performance.word(0x2b8c, 100)?; + performance.word(0x2b98, pwr.se_target)?; + performance.word( + 0x2ba4, + pwr.se_filter_time_constant + .checked_mul(period) + .ok_or(EOVERFLOW)?, + )?; + performance.word( + 0x2ba8, + pwr.se_filter_time_constant_1 + .checked_mul(period) + .ok_or(EOVERFLOW)?, + )?; + performance.word(0x2bac, clocks(pwr.se_filter_time_constant)?)?; + performance.word(0x2bb4, clocks(pwr.se_filter_time_constant_1)?)?; + performance.float(0x2b94, f32!(8000.0))?; + for offset in [0x2bc0, 0x2bc4, 0x2c30] { + performance.float(offset, f32!(65536.0))?; + } + performance.word(0x2be8, 8000)?; + performance.word(0x2bec, 4)?; + performance.word(0x2bf0, pwr.se_inactive_threshold)?; + performance.word(0x2bf4, pwr.se_engagement_criteria as u32)?; + performance.word(0x2bf8, 2)?; + performance.word(0x2bfc, 4)?; + performance.word(0x2c00, pwr.se_reset_criteria)?; + performance.word(0x2c40, pwr.avg_power_min_duty_cycle)?; + performance.word(0x2e2c, 4)?; + performance.float(0x2e38, f32!(0.96875))?; + performance.float(0x2e40, f32!(0.03125))?; + performance.float(0x2e48, f32!(5.0))?; + performance.float(0x2e54, f32!(65536.0))?; + performance.float(0x2e58, f32!(15.0))?; + performance.word(0x2e64, 700)?; + for offset in [0x2e68, 0x2e6c, 0x2e78] { + performance.word(offset, ceiling)?; + } + performance.word(0x2f08, 0x4248)?; + performance.word(0x2f10, 125)?; + performance.word(0x2f14, 1)?; + performance.pointer(0x3b50, sensor)?; + performance.word(0x3b58, 0x4b48)?; + performance.word(0x3b60, 1)?; + performance.word(0x62a8, 1)?; + Ok(Self { + hardware: main.0, + performance: performance.0, + power: power.0, + globals: globals.0, + maximum_khz: top.freq_hz / 1000, + }) + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_profile.rs b/drivers/gpu/drm/asahi/m3/m3_profile.rs new file mode 100644 index 00000000000000..04b872ed9ea809 --- /dev/null +++ b/drivers/gpu/drm/asahi/m3/m3_profile.rs @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Firmware-interface properties, independent of the physical G15 GPU record format. + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Profile { + Legacy, + Extended, +} + +pub(crate) const PARAMS_REQUIRED: u64 = 600; + +impl Profile { + pub(crate) const fn firmware_compat(self) -> [u32; 3] { + match self { + Self::Legacy => [14, 8, 3], + Self::Extended => [26, 6, 2], + } + } + + pub(crate) const fn hal(self) -> u32 { + match self { + Self::Legacy => 0, + Self::Extended => 200, + } + } + + pub(crate) const fn user_end(self) -> u64 { + match self { + Self::Legacy => 0x2ff_ffff_8000, + Self::Extended => (1 << 42) - 2 * 0x4000, + } + } + + /// Keep the diagnostic AP/GPU timeline in its established 24 MHz units. + /// User timestamps and duration calculations retain native firmware units. + pub(crate) const fn profiler_ticks(self, timestamp: u64) -> u64 { + match self { + Self::Legacy => timestamp, + Self::Extended => (timestamp / 1000) * 24 + (timestamp % 1000) * 24 / 1000, + } + } + + pub(crate) fn gpu_duration_ns(self, elapsed: u64) -> Option { + match self { + Self::Legacy => elapsed.checked_mul(1000).map(|value| value / 24), + Self::Extended => Some(elapsed), + } + } + + pub(crate) const fn vm_acknowledged(self, params_bytes: u64, flags: u32) -> bool { + // Both profiles consume G15 records; the existing VM padding stays zero. + params_bytes >= PARAMS_REQUIRED && flags == 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn profiling_uses_one_clock_domain_without_changing_native_durations() { + for value in [0, 1, 999, 1000, 24_000_000, u64::MAX] { + assert_eq!(Profile::Legacy.profiler_ticks(value), value); + assert_eq!( + Profile::Extended.profiler_ticks(value), + ((value as u128) * 24 / 1000) as u64 + ); + } + assert_eq!(Profile::Extended.profiler_ticks(1_000_000_000), 24_000_000); + } + + #[test] + fn profile_acknowledgement_is_exact_and_file_query_covers_hal() { + for profile in [Profile::Legacy, Profile::Extended] { + let expected = 0; + for size in [0, 599, 600, 616, u64::MAX] { + for flags in [0, 1, 2, 3, u32::MAX] { + assert_eq!( + profile.vm_acknowledged(size, flags), + size >= 600 && flags == expected + ); + } + } + } + } + + #[test] + fn firmware_interface_version_does_not_change_physical_identity() { + assert_eq!(Profile::Legacy.hal(), 0); + assert_eq!(Profile::Extended.hal(), 200); + assert_eq!(Profile::Legacy.firmware_compat(), [14, 8, 3]); + assert_eq!(Profile::Extended.firmware_compat(), [26, 6, 2]); + assert_eq!(Profile::Extended.user_end(), 0x3ff_ffff_8000); + assert_eq!(Profile::Legacy.user_end(), 0x2ff_ffff_8000); + } + #[test] + fn native_nanoseconds_are_never_rescaled_as_legacy_ticks() { + assert_eq!( + Profile::Extended.gpu_duration_ns(24_000_000), + Some(24_000_000) + ); + assert_eq!( + Profile::Legacy.gpu_duration_ns(24_000_000), + Some(1_000_000_000) + ); + assert_eq!(Profile::Extended.gpu_duration_ns(u64::MAX), Some(u64::MAX)); + assert_eq!(Profile::Legacy.gpu_duration_ns(u64::MAX), None); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_queue_layout.rs b/drivers/gpu/drm/asahi/m3/m3_queue_layout.rs index 39cc1f5f2e4908..c3411bfdff8ed2 100644 --- a/drivers/gpu/drm/asahi/m3/m3_queue_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_queue_layout.rs @@ -113,3 +113,44 @@ pub(crate) fn ring_entry(index: u32, capacity: u32, seeds: &[FirmwareVa]) -> Res if seeds.is_empty() || seeds.len() > capacity as usize || capacity > 65536 || index >= capacity { return Err(Error::Index); } Ok(if (index as usize) < seeds.len() { seeds[index as usize].0.to_le_bytes() } else { [0xcc; 8] }) } + +impl QueueInfo { + pub(crate) const EXTENDED_SIZE: usize = 0x24c0; + + /// Scheduler storage belongs to this same retained allocation; it is not + /// interchangeable with the legacy GPU scratch object. + pub(crate) fn encode_extended(&self, owner: FirmwareVa, output: &mut [u8]) -> Result<(), Error> { + let end = owner.get().checked_add(Self::EXTENDED_SIZE as u64).ok_or(Error::Address)?; + if end > 0xffff_fc30_0000_0000 { return Err(Error::Address); } + let bytes = output.get_mut(..Self::EXTENDED_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + for (offset, value) in [(0, self.state.get()), (8, self.ring.get()), + (16, self.job_list.get()), (24, owner.get() + 0xb0), (0xa4, self.context.get())] { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + } + for offset in [0x2c, 0x4c] { bytes[offset..offset + 4].fill(0xff); } + for offset in [0x30, 0x34, 0x48] { + bytes[offset..offset + 4].copy_from_slice(&2u32.to_le_bytes()); + } + bytes[0x38..0x40].copy_from_slice(&0xffff_0000_0000_0000u64.to_le_bytes()); + Ok(()) + } +} + +#[cfg(test)] +mod extended_tests { + use super::*; + #[test] fn scheduler_pointer_relocates_with_its_retained_queue_owner() { + let fw = |i: u64| FirmwareVa::new(0xffff_fc21_4000_0000 + i * 0x4000).unwrap(); + let info = QueueInfo { state: fw(1), ring: fw(2), job_list: fw(3), + gpu_scratch: fw(4), context: fw(5), uuid: 9 }; + let mut output = vec![0xa5; QueueInfo::EXTENDED_SIZE + 8]; + info.encode_extended(fw(0), &mut output).unwrap(); + assert_eq!(u64::from_le_bytes(output[24..32].try_into().unwrap()), fw(0).get() + 0xb0); + assert_eq!(&output[0xb0..QueueInfo::EXTENDED_SIZE], &vec![0; QueueInfo::EXTENDED_SIZE - 0xb0]); + assert_eq!(&output[QueueInfo::EXTENDED_SIZE..], &[0xa5; 8]); + let mut short = [0xa5; INFO_SIZE]; + assert!(info.encode_extended(fw(0), &mut short).is_err()); + assert_eq!(short, [0xa5; INFO_SIZE]); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_render.rs b/drivers/gpu/drm/asahi/m3/m3_render.rs index e7a02982e96f12..9a9f63d15b784f 100644 --- a/drivers/gpu/drm/asahi/m3/m3_render.rs +++ b/drivers/gpu/drm/asahi/m3/m3_render.rs @@ -26,7 +26,7 @@ pub(crate) struct Render { passes:KVec, init_bm:Buffer, // All active mappings above must drop before their ASID binding. _binding:mmu::VmBind, - stats: Region, sequence_bytes: KVec, tiler_bytes: KVec, fragment_bytes: KVec, + stats: [Region;2], profile:super::m3_profile::Profile, sequence_bytes: KVec, tiler_bytes: KVec, fragment_bytes: KVec, last_progress_ns:i64, draw:u64, heads:[u16;2], cached_views:KVec, slot:usize, batch_count:usize, early_count:u32, @@ -67,37 +67,45 @@ const RENDER_QUEUES: [QueueObjects; 2] = [ scratch: s::FRAGMENT_SCRATCH, tiler: false, uuid: 0x3d0000 }, ]; impl Render { - pub(crate) fn new(dev:&driver::AsahiDrmDevice,uat:&mmu::Uat,vm:&mmu::Vm,stats:Region,clusters:u32, - registers:crate::m3::m3_compute_layout::RegisterSet)->Result { + pub(crate) fn new(dev:&driver::AsahiDrmDevice,uat:&mmu::Uat,vm:&mmu::Vm,stats:[Region;2],clusters:u32, + registers:crate::m3::m3_compute_layout::RegisterSet,profile:super::m3_profile::Profile)->Result { let lower_tlbi=crate::m3::m3_memory::LowerTlbi; let binding=uat.bind(vm)?; let mut objects=KVec::new();let mut aliases=KVec::new(); for index in 0..s::COUNT { - let a=s::allocation(index).map_err(|_|EINVAL)?; + let a=s::allocation_for(profile,index).map_err(|_|EINVAL)?; let protection=prot::Prot::from_pte(a.access.pte()); let b=match a.space { Space::ClientGpu=>Buffer::at_prot(dev,uat.kernel_vm(),Some((vm,a.address)),None, a.size,prot::PROT_GPU_FW_SHARED_RW,protection)?, - Space::Firmware=>Buffer::at_prot(dev,uat.kernel_vm(),None,Some(a.address), + Space::Firmware=>Buffer::at_prot(dev,uat.kernel_vm(), + a.gpu_alias.map(|address|(uat.kernel_lower_vm(),address)),Some(a.address), a.size,protection,prot::PROT_GPU_SHARED_RW)?, }; + let mut b=b; + if a.gpu_alias.is_some() { aliases.push(b.map_gpu_view(vm)?,GFP_KERNEL)?; } objects.push(b,GFP_KERNEL)?; } // InitBM is shared, not part of a pass. Preserve its qualified mapping. - let init_bm=Buffer::at_prot(dev,uat.kernel_vm(),None,Some(0xfffffc200062bfe0), + let init_bm=Buffer::at_prot(dev,uat.kernel_vm(),None,Some(if profile==super::m3_profile::Profile::Extended {0xffff_fc24_4000_0000} + else {0xfffffc200062bfe0}), sync::INIT_BM_SIZE,prot::PROT_FW_SHARED_RW,prot::PROT_GPU_SHARED_RW)?; let mut passes=KVec::with_capacity(pass_layout::SLOTS,GFP_KERNEL)?; - for slot in 0..pass_layout::SLOTS {passes.push(Pass::new(dev,uat,vm,slot,clusters,&mut aliases)?,GFP_KERNEL)?;} + for slot in 0..pass_layout::SLOTS {passes.push(Pass::new(dev,uat,vm,slot,clusters,profile,&mut aliases)?,GFP_KERNEL)?;} let mut sequence_bytes=KVec::new(); sequence_bytes.resize(sequence::STORAGE,0u8,GFP_KERNEL)?; let mut tiler_bytes=KVec::new(); - tiler_bytes.resize(tiler::SIZE,0u8,GFP_KERNEL)?; + tiler_bytes.resize(if profile==super::m3_profile::Profile::Extended {tiler::EXTENDED_SIZE} else {tiler::SIZE},0u8,GFP_KERNEL)?; let mut fragment_bytes=KVec::new(); - fragment_bytes.resize(fragment::SIZE,0u8,GFP_KERNEL)?; - let mut job=Self{passes,init_bm,stats,sequence_bytes,tiler_bytes,fragment_bytes,objects,_aliases:aliases,_binding:binding,last_progress_ns:0,draw:0,heads:[0;2],cached_views:KVec::new(),slot:0,batch_count:0,early_count:0,stage_timestamps:[true;2],base:0,checkpoint:(0,[0;2],0,0,0,0),clusters,registers,_lower_tlbi:lower_tlbi}; + fragment_bytes.resize(if profile==super::m3_profile::Profile::Extended {fragment::EXTENDED_SIZE} else {fragment::SIZE},0u8,GFP_KERNEL)?; + let mut job=Self{passes,init_bm,stats,profile,sequence_bytes,tiler_bytes,fragment_bytes,objects,_aliases:aliases,_binding:binding,last_progress_ns:0,draw:0,heads:[0;2],cached_views:KVec::new(),slot:0,batch_count:0,early_count:0,stage_timestamps:[true;2],base:0,checkpoint:(0,[0;2],0,0,0,0),clusters,registers,_lower_tlbi:lower_tlbi}; // Lists and the matching manager are built before InitBM publication. job.initialize_parameter_buffer()?; - for pool in &UMA_POOLS { pool.initialize(&mut job.objects)?; } + for (index,pool) in UMA_POOLS.iter().enumerate() { + if profile==super::m3_profile::Profile::Extended { + pool.initialize_extended(&mut job.objects,33+index as u8,false)?; + } else {pool.initialize(&mut job.objects)?;} + } job.initialize_state()?; job.initialize_queues()?; dev_info!(dev.as_ref(),"M3 native render storage initialized, PB={} MiB\n",PB_BLOCKS as usize*PB_BLOCK_SIZE/(1024*1024)); @@ -113,8 +121,14 @@ impl Render { let control = queue::FirmwareVa::new(self.objects[BLOCK_CONTROL].va()).map_err(|_| EINVAL)?; let counter = queue::FirmwareVa::new(self.objects[s::BM_COUNTER].va()).map_err(|_| EINVAL)?; let manager = parameter::Manager::new(pages, blocks, control, counter, PB_BLOCKS).map_err(|_| EINVAL)?; - let mut bytes = [0; parameter::MANAGER_SIZE]; - manager.encode(&mut bytes).map_err(|_| EINVAL)?; + let extended=self.profile==super::m3_profile::Profile::Extended; + let mut bytes = [0; parameter::Manager::EXTENDED_SIZE]; + let length=if extended { + manager.encode_extended(queue::FirmwareVa::new(self.objects[PARAMETER_PAGE_LIST].va()).map_err(|_|EINVAL)?, + queue::FirmwareVa::new(self.objects[PARAMETER_BLOCK_LIST].va()).map_err(|_|EINVAL)?,0,&mut bytes) + .map_err(|_|EINVAL)?; + parameter::Manager::EXTENDED_SIZE + } else {manager.encode(&mut bytes).map_err(|_|EINVAL)?;parameter::MANAGER_SIZE}; for group in 0..PB_GROUPS { let buffer = &self.objects[PB_FIRST + group]; let backing = parameter::Backing::new(GpuRegion::new(buffer.gpu_va()?, buffer.size()).map_err(|_| EINVAL)?) @@ -128,18 +142,30 @@ impl Render { backing.page(page).map_err(|_| EINVAL)?)?; } } - self.objects[BUFFER_MANAGER].write(0, &bytes) + self.objects[BUFFER_MANAGER].write(0, &bytes[..length]) } fn initialize_state(&mut self) -> Result { let address = |owner: usize| queue::FirmwareVa::new(self.objects[owner].va()).map_err(|_| EINVAL); let notification = state::Notifier { threshold: address(s::EVENT_COUNT)?, context: self._binding.slot() }; let list_owner = address(JOB_LIST)?; let mut context = [0; state::CONTEXT_SIZE]; - state::context(&mut context).map_err(|_| EINVAL)?; - self.objects[GPU_CONTEXT].write(0, &context)?; - let mut notifier = [0; state::NOTIFIER_SIZE]; - notification.encode(&mut notifier).map_err(|_| EINVAL)?; - self.objects[NOTIFIER].write(0, ¬ifier)?; + if self.profile==super::m3_profile::Profile::Extended { + state::context_extended(&mut context).map_err(|_|EINVAL)?; + self.objects[GPU_CONTEXT].write(0,&context[..state::EXTENDED_CONTEXT_SIZE])?; + for engine in 0..2 { + let value=state::Notifier {threshold:queue::FirmwareVa::new(self.objects[s::EVENT_COUNT].va()+engine as u64*8).map_err(|_|EINVAL)?, + context:self._binding.slot()}; + let mut notifier=[0;state::EXTENDED_NOTIFIER_SIZE]; + value.encode_extended(&mut notifier).map_err(|_|EINVAL)?; + self.objects[NOTIFIER].write(engine*state::EXTENDED_NOTIFIER_SIZE,¬ifier)?; + } + } else { + state::context(&mut context).map_err(|_| EINVAL)?; + self.objects[GPU_CONTEXT].write(0, &context)?; + let mut notifier = [0; state::NOTIFIER_SIZE]; + notification.encode(&mut notifier).map_err(|_| EINVAL)?; + self.objects[NOTIFIER].write(0, ¬ifier)?; + } let mut list = [0; state::JOB_LIST_SIZE]; state::job_list(&mut list, list_owner).map_err(|_| EINVAL)?; self.objects[JOB_LIST].write(0, &list)?; @@ -157,6 +183,10 @@ impl Render { state::counter(bytes, value).map_err(|_| EINVAL)?; self.objects[owner].write(0, bytes)?; } + if self.profile==super::m3_profile::Profile::Extended { + self.objects[s::EVENT_COUNT].fill(0)?; + self.objects[s::BM_COUNTER].fill(0)?; + } Ok(()) } fn initialize_queues(&mut self) -> Result { @@ -172,8 +202,13 @@ impl Render { }else{[self.passes[0].get(P::TilerToFragment).va(),self.passes[0].get(P::FragmentCommand).va()]}; let seeds=[queue::FirmwareVa::new(seed_addresses[0]).map_err(|_|EINVAL)?, queue::FirmwareVa::new(seed_addresses[1]).map_err(|_|EINVAL)?]; - let mut bytes = [0; queue::INFO_SIZE]; - info.encode(&mut bytes).map_err(|_| EINVAL)?; + let mut bytes=KVec::new(); + bytes.resize(if self.profile==super::m3_profile::Profile::Extended { + queue::QueueInfo::EXTENDED_SIZE + } else {queue::INFO_SIZE},0u8,GFP_KERNEL)?; + if self.profile==super::m3_profile::Profile::Extended { + info.encode_extended(address(owner.info)?,&mut bytes).map_err(|_|EINVAL)?; + } else {info.encode(&mut bytes).map_err(|_|EINVAL)?;} self.objects[owner.info].write(0, &bytes)?; let mut state = [0; queue::STATE_SIZE]; queue::RingState { capacity: queue::QUALIFIED_CAPACITY, done: 0, read: 0, write: 0 } @@ -187,9 +222,12 @@ impl Render { Ok(()) } fn write_barrier(&mut self, owner: P, command: sync::Barrier) -> Result { - let mut bytes = [0; sync::BARRIER_SIZE]; - command.encode(&mut bytes).map_err(|_| EINVAL)?; - self.passes[self.slot].get_mut(owner).write(0, &bytes) + let mut bytes=[0;sync::Barrier::EXTENDED_SIZE]; + let length=if self.profile==super::m3_profile::Profile::Extended { + command.encode_extended(&mut bytes).map_err(|_|EINVAL)?; + sync::Barrier::EXTENDED_SIZE + } else {command.encode(&mut bytes).map_err(|_|EINVAL)?;sync::BARRIER_SIZE}; + self.passes[self.slot].get_mut(owner).write(0,&bytes[..length]) } // Construct synchronization from the actual owners after resetting a pass, // before publishing its ring entries. InitBM is shared and dispatched only @@ -201,9 +239,11 @@ impl Render { let ta_stamp = address(s::TA_FW_STAMP)?; let fs_stamp = address(s::FRAGMENT_FW_STAMP)?; let mut init = [0; sync::INIT_BM_SIZE]; - sync::InitBufferManager { context: self._binding.slot(), slot: 0, - block_count: PB_BLOCKS, manager, stamp: timeline::stamp(s::STAMP_TA - 256, 1) } - .encode(&mut init).map_err(|_| EINVAL)?; + let value=sync::InitBufferManager { context: self._binding.slot(), slot: 0, + block_count: PB_BLOCKS, manager, stamp: timeline::stamp(s::STAMP_TA - 256, 1) }; + if self.profile==super::m3_profile::Profile::Extended { + value.encode_extended(&mut init).map_err(|_|EINVAL)?; + } else {value.encode(&mut init).map_err(|_|EINVAL)?;} self.init_bm.write(0, &init)?; let ta_value = timeline::stamp(s::STAMP_TA - 256, draw); let fs_value = timeline::stamp(s::STAMP_FRAGMENT - 256, draw); @@ -225,7 +265,7 @@ impl Render { } // Prepare each private command after its retired slot is reset. The // reusable host staging allocation is copied before the next slot is built. - fn prepare_tiler(&mut self,r:agx_uapi::UapiRenderCommand,draw:u64)->Result { + fn prepare_tiler(&mut self,r:agx_uapi::UapiRenderCommand,usc:u64,draw:u64)->Result { use crate::m3::m3_compute_layout::GpuVa; let pass=&self.passes[self.slot]; let shared=|index:usize| queue::FirmwareVa::new(self.objects[index].va()).map_err(|_|EINVAL); @@ -243,7 +283,7 @@ impl Render { let value=tiler::Command { context:self._binding.slot(),register_set:self.registers, notifier:shared(NOTIFIER)?,manager:shared(BUFFER_MANAGER)?, - scene:fw(P::Scene)?,empty:shared(547)?,gpu_alias:GpuVa::new(command.gpu_va()?).map_err(|_|EINVAL)?, + scene:fw(P::Scene)?,empty:if self.profile==super::m3_profile::Profile::Extended {queue::FirmwareVa::new(pass.get(P::Scene).va()+0x100).map_err(|_|EINVAL)?} else {shared(547)?},gpu_alias:GpuVa::new(command.gpu_va()?).map_err(|_|EINVAL)?, sequence:Region::new(seq.va(),seq.size()).map_err(|_|EINVAL)?,pool:shared(UMA_POOLS[0].manager)?, scratch:fw(P::TilerScratch)?,stamp:shared(s::TA_STAMP)?,fw_stamp:shared(s::TA_FW_STAMP)?, stamp_value:timeline::stamp(s::STAMP_TA - 256, draw),timestamps:[fw(P::TilerStart)?,fw(P::TilerEnd)?], @@ -253,7 +293,12 @@ impl Render { vdm:crate::m3::agx_render_state::compact(r.vdm_base).map_err(|_|EINVAL)?, multisample:r.multisample_control,ppp:r.ppp_control,process_empty_tiles:r.flags&2!=0, }; - value.encode(&mut self.tiler_bytes).map_err(|_|EINVAL)?; + if self.profile==super::m3_profile::Profile::Extended { + value.encode_extended(tiler::Dispatch26 {incarnation:self._binding.generation(),serial:draw.checked_mul(2).ok_or(EOVERFLOW)?, + peer_stamp:timeline::stamp(s::STAMP_FRAGMENT-256,draw),usc_window:usc, + firmware_aux:queue::FirmwareVa::new(self.objects[UMA_POOLS[0].manager].va()+0x100).map_err(|_|EINVAL)?, + scene_scratch:gpu(P::FragmentScratch)?},&mut self.tiler_bytes).map_err(|_|EINVAL)?; + } else {value.encode(&mut self.tiler_bytes).map_err(|_|EINVAL)?;} self.passes[self.slot].get_mut(P::TilerCommand).write(0,&self.tiler_bytes) } fn prepare_sequences(&mut self,draw:u64)->Result { @@ -266,14 +311,21 @@ impl Render { let notifier=&self.objects[NOTIFIER]; let owners=sequence::Owners { command:region(if fs{P::FragmentCommand}else{P::TilerCommand})?,sequence:region(target)?, - stats:self.stats,notifier:Region::new(notifier.va(),notifier.size()).map_err(|_|EINVAL)?, + stats:self.stats[usize::from(fs)],notifier:Region::new(notifier.va()+ + if fs && self.profile==super::m3_profile::Profile::Extended {0x200} else {0}, + if self.profile==super::m3_profile::Profile::Extended {0x200} else {notifier.size()}).map_err(|_|EINVAL)?, queue:shared(if fs{s::FRAGMENT_QUEUE}else{s::TA_QUEUE})?,scene:fw(P::Scene)?, manager:shared(BUFFER_MANAGER)?,pool:shared(UMA_POOLS[usize::from(fs)].manager)?, fw_stamp:shared(if fs{s::FRAGMENT_FW_STAMP}else{s::TA_FW_STAMP})?, context:self._binding.slot(),stamp:timeline::stamp((if fs{s::STAMP_FRAGMENT}else{s::STAMP_TA}) - 256, draw), }; let timed=self.stage_timestamps[usize::from(fs)]; - let length=if fs { + let length=if self.profile==super::m3_profile::Profile::Extended { + let pool=&self.objects[UMA_POOLS[usize::from(fs)].manager]; + let auxiliary=super::m3_compute_layout::GpuVa::new(pool.gpu_va()?+if fs{0x200}else{0x100}).map_err(|_|EINVAL)?; + let serial=draw.checked_mul(2).and_then(|v|v.checked_sub(u64::from(fs))).ok_or(EOVERFLOW)?; + owners.encode_extended(fs,auxiliary,serial,&mut self.sequence_bytes).map_err(|_|EINVAL)? + } else if fs { let b=pass.get(P::FragmentScratch); let scratch=GpuRegion::new(b.gpu_va()?,b.size()).map_err(|_|EINVAL)?; sequence::Fragment{owners,scratch,queue_command_count:timeline::previous(draw)}.encode_timestamps(&mut self.sequence_bytes,timed).map_err(|_|EINVAL)? @@ -290,6 +342,9 @@ impl Render { fn patch_context(&mut self)->Result { let context=self._binding.slot(); self.objects[NOTIFIER].u32(state::NOTIFIER_CONTEXT, context)?; + if self.profile==super::m3_profile::Profile::Extended { + self.objects[NOTIFIER].u32(0x200+state::NOTIFIER_CONTEXT,context)?; + } Ok(()) } /// Pass geometry; TPC storage is per tiler accelerator, one per GPU cluster. @@ -320,7 +375,8 @@ impl Render { let value=fragment::Command { context:self._binding.slot(),register_set:self.registers, sequence:Region::new(seq.va(),seq.size()).map_err(|_|EINVAL)?, - notifier:shared(NOTIFIER)?,manager:shared(BUFFER_MANAGER)?,scene:fw(P::Scene)?,empty:shared(547)?, + notifier:queue::FirmwareVa::new(self.objects[NOTIFIER].va()+ + if self.profile==super::m3_profile::Profile::Extended {0x200} else {0}).map_err(|_|EINVAL)?,manager:shared(BUFFER_MANAGER)?,scene:fw(P::Scene)?,empty:if self.profile==super::m3_profile::Profile::Extended {queue::FirmwareVa::new(pass.get(P::Scene).va()+0x100).map_err(|_|EINVAL)?} else {shared(547)?}, gpu_alias:GpuVa::new(command.gpu_va()?).map_err(|_|EINVAL)?,tilemap:gpu(P::Tilemap)?, heap:gpu(P::HeapMetadata)?,auxiliary:gpu(P::Auxiliary)?,scene_user:gpu(P::SceneUser)?, scratch:gpu(P::FragmentScratch)?,pool:shared(UMA_POOLS[1].manager)?,stamp:shared(s::FRAGMENT_STAMP)?, @@ -341,15 +397,34 @@ impl Render { stencil_clear:r.stencil_clear,sample_size:u32::from(r.sample_size),process_empty_tiles:r.flags&2!=0, }, }; - value.encode(&mut self.fragment_bytes).map_err(|_|EINVAL)?; + if self.profile==super::m3_profile::Profile::Extended { + value.encode_extended(tiler::Dispatch26 {incarnation:self._binding.generation(),serial:draw.checked_mul(2).and_then(|v|v.checked_sub(1)).ok_or(EOVERFLOW)?, + peer_stamp:0,usc_window:usc, + firmware_aux:queue::FirmwareVa::new(self.objects[UMA_POOLS[1].manager].va()+0x200).map_err(|_|EINVAL)?, + scene_scratch:gpu(P::FragmentScratch)?},&mut self.fragment_bytes).map_err(|_|EINVAL)?; + } else {value.encode(&mut self.fragment_bytes).map_err(|_|EINVAL)?;} self.passes[self.slot].get_mut(P::FragmentCommand).write(0,&self.fragment_bytes) } + pub(crate) fn set_attachments(&mut self,attachments:&[crate::m3::agx_attachments::Attachments;2])->Result { + // Preserve the legacy renderer's existing packet format. The native + // interface consumes separate attachment apertures for both stages. + if self.profile!=super::m3_profile::Profile::Extended {return Ok(());} + for (fragment,list) in attachments.iter().enumerate() { + let field=if fragment==0 {P::TilerSequence}else{P::FragmentSequence}; + let buffer=self.passes[self.slot].get_mut(field); + sequence::patch_extended_attachments(fragment!=0,list,|offset,data|buffer.write(offset,data))?; + } + crate::m3::agx_memory::publish();Ok(()) + } pub(crate) fn set_user_timestamps(&mut self,addresses:[[u64;2];2])->Result { if addresses.iter().enumerate().any(|(stage,pair)|*pair!=[0;2] && !self.stage_timestamps[stage]) {return Err(EINVAL);} for (stage,(command,sequence,offset,start,end)) in [ (P::TilerCommand,P::TilerSequence,sequence::ta::USER_PAIR,sequence::ta::TIMESTAMP_START,sequence::ta::TIMESTAMP_END), (P::FragmentCommand,P::FragmentSequence,sequence::fragment::USER_PAIR,sequence::fragment::TIMESTAMP_START,sequence::fragment::TIMESTAMP_END) ].into_iter().enumerate() { + let (offset,start,end)=if self.profile==super::m3_profile::Profile::Extended { + if stage==0 {(0x8d8,0x1cc,0x21c)} else {(0xc20,0x1e4,0x234)} + } else {(offset,start,end)}; let pair=addresses[stage]; if !self.stage_timestamps[stage] {continue;} self.passes[self.slot].get_mut(command).u64(offset,pair[0])?; @@ -365,19 +440,19 @@ impl Render { pub(crate) fn gpu_ns(&mut self,base:usize,count:usize)->Result { let start=self.passes[base].get_mut(P::TilerStart).read_u64(0)?; let end=self.passes[Self::at(base,count-1)].get_mut(P::FragmentEnd).read_u64(0)?; - Ok(if start!=0 && end>=start {(end-start)*1000/24} else {0}) + self.profile.gpu_duration_ns(if start!=0 && end>=start {end-start} else {0}).ok_or(EOVERFLOW) } /// The batch's first tiler start and last fragment end, in 24 MHz GPU timestamp ticks. pub(crate) fn gpu_ticks(&mut self,base:usize,count:usize)->Result<(u64,u64)> { - Ok((self.passes[base].get_mut(P::TilerStart).read_u64(0)?, - self.passes[Self::at(base,count-1)].get_mut(P::FragmentEnd).read_u64(0)?)) + Ok((self.profile.profiler_ticks(self.passes[base].get_mut(P::TilerStart).read_u64(0)?), + self.profile.profiler_ticks(self.passes[Self::at(base,count-1)].get_mut(P::FragmentEnd).read_u64(0)?))) } /// Read retained timestamps only after exact batch retirement. The first /// TA and final fragment delimit the batch in the shared 24 MHz counter. pub(crate) fn batch_gpu_span(&mut self)->Result<[u64;2]> { if self.batch_count == 0 {return Err(EINVAL);} - Ok([self.passes[self.base].get_mut(P::TilerStart).read_u64(0)?, - self.passes[Self::at(self.base,self.batch_count-1)].get_mut(P::FragmentEnd).read_u64(0)?]) + let (start,end)=self.gpu_ticks(self.base,self.batch_count)?; + Ok([start,end]) } pub(crate) fn stage_ns(&mut self,base:usize,count:usize)->Result<[u64;3]> { let mut total=[0;3]; @@ -390,7 +465,7 @@ impl Render { pub(crate) fn slot_stage_ns(&mut self,base:usize,count:usize,index:usize)->Result<[u64;3]> { if index>=count {return Err(EINVAL);} let slot=Self::at(base,index); - let duration=|a:u64,b:u64| if a!=0 && b>=a {(b-a)*1000/24} else {0}; + let duration=|a:u64,b:u64| self.profile.gpu_duration_ns(if a!=0 && b>=a {b-a} else {0}).unwrap_or(0); let fs_start=self.passes[slot].get_mut(P::FragmentStart).read_u64(0)?; let fs_end=self.passes[slot].get_mut(P::FragmentEnd).read_u64(0)?; let ta_start=self.passes[slot].get_mut(P::TilerStart).read_u64(0)?; @@ -415,7 +490,9 @@ impl Render { } /// Whether any draw has been published (the first one initializes the buffer manager). pub(crate) fn started(&self)->bool {self.draw>0} - pub(crate) fn first(&self)->bool {self.draw==self.batch_count as u64} + pub(crate) fn first(&self)->bool { + self.profile==super::m3_profile::Profile::Legacy && self.draw==self.batch_count as u64 + } /// Start a batch in pass slots `base..`. With `overlap`, earlier batches may still run: then /// the VM must already be bound, no storage may grow and the slots must not be in flight /// (the caller keeps the in-flight passes plus this batch within SLOTS). @@ -447,13 +524,15 @@ impl Render { let binding=uat.bind(vm)?; let mut views=KVec::new();let mut aliases=KVec::new(); for i in 0..s::COUNT { - if s::allocation(i).map_err(|_|EINVAL)?.space==Space::ClientGpu { + let a=s::allocation_for(self.profile,i).map_err(|_|EINVAL)?; + if a.gpu_alias.is_some() {aliases.push(self.objects[i].map_gpu_view(vm)?,GFP_KERNEL)?;} + if a.space==Space::ClientGpu { views.push((ViewOwner::Shared(i),self.objects[i].map_gpu_view(vm)?),GFP_KERNEL)?; } } for (slot,pass) in self.passes.iter_mut().enumerate() { for field in pass_layout::FIELDS { - let a=pass_layout::board_allocation(slot,field,self.clusters).map_err(|_|EINVAL)?; + let a=pass_layout::allocation_for(self.profile,slot,field,self.clusters).map_err(|_|EINVAL)?; if a.space==Space::ClientGpu {views.push((ViewOwner::Pass{slot,field},pass.get_mut(field).map_gpu_view(vm)?),GFP_KERNEL)?;} if a.gpu_alias.is_some() {aliases.push(pass.get_mut(field).map_gpu_view(vm)?,GFP_KERNEL)?;} } @@ -498,12 +577,22 @@ impl Render { self.base=base; Ok(()) } + fn publish_counts(&mut self, draw:u64) -> Result { + if self.profile==super::m3_profile::Profile::Extended { + for offset in [0,8] {self.objects[s::EVENT_COUNT].u64(offset,draw)?;} + for counter in [TA_POOL_COUNTER,FRAGMENT_POOL_COUNTER] {self.objects[counter].u64(0,draw)?;} + } else { + self.objects[s::BM_COUNTER].u32(0,draw as u32)?; + self.objects[s::EVENT_COUNT].u32(0,timeline::events(draw,2))?; + } + Ok(()) + } + /// Restore producer state only when no batch is in flight. Running firmware can /// consume append's WRITE updates without a new doorbell, so overlap cannot abort. pub(crate) fn abort_batch(&mut self)->Result { (self.draw,self.heads,self.batch_count,self.slot,self.early_count,self.base)=self.checkpoint; - self.objects[s::BM_COUNTER].u32(0,self.draw as u32)?; - self.objects[s::EVENT_COUNT].u32(0,timeline::events(self.draw,2))?; + self.publish_counts(self.draw)?; for (stage,pointers) in [s::TA_POINTERS,s::FRAGMENT_POINTERS].into_iter().enumerate() { self.objects[pointers].u32(queue::WRITE,u32::from(self.heads[stage]))?; } @@ -547,8 +636,20 @@ impl Render { self.draw=self.draw.checked_add(1).ok_or(EOVERFLOW)?; self.batch_count+=1; let draw=self.draw; - self.objects[s::BM_COUNTER].u32(0,draw as u32)?; - self.objects[s::EVENT_COUNT].u32(0,timeline::events(draw,2))?; + self.publish_counts(draw)?; + if self.profile==super::m3_profile::Profile::Extended { + let pass=&self.passes[self.slot]; + let scratch=pass.get(P::FragmentScratch); + let scratch_gpu=GpuRegion::new(scratch.gpu_va()?,scratch.size()).map_err(|_|EINVAL)?; + let scratch_fw=queue::FirmwareVa::new(scratch.va()).map_err(|_|EINVAL)?; + let list=pass.get(P::SceneUser); + let list=GpuRegion::new(list.gpu_va()?,list.size()).map_err(|_|EINVAL)?; + let metadata=pass.get(P::Scene); + let owner=Region::new(metadata.va(),metadata.size()).map_err(|_|EINVAL)?; + let mut bytes=[0;scene::EXTENDED_SIZE]; + scene::encode_extended(&mut bytes,owner,scratch_gpu,scratch_fw,list).map_err(|_|EINVAL)?; + self.passes[self.slot].get_mut(P::Scene).write(0,&bytes)?; + } else { let entry_offset = (self.draw % u64::from(MANAGER_SCENE_COUNT)) as usize * 4; let scene_list = &mut self.objects[s::BM_SCENES]; if entry_offset + 4 > scene_list.size() { return Err(ERANGE); } @@ -560,7 +661,8 @@ impl Render { let mut bytes = [0; scene::SIZE]; scene::Scene { manager_entry, manager_misc, user_buffer }.encode(&mut bytes).map_err(|_| EINVAL)?; self.passes[self.slot].get_mut(P::Scene).write(0, &bytes)?; - self.prepare_tiler(r,draw)?; + } + self.prepare_tiler(r,usc,draw)?; self.patch_context()?; self.prepare_fragment(r,usc,draw)?; self.prepare_sequences(draw)?; @@ -655,7 +757,13 @@ impl Render { if self.objects[stamp].read_u32(0)?!=value || self.objects[fw].read_u32(0)?!=value || self.objects[pointers].read_u32(queue::DONE)?!=u32::from(head) || self.objects[pointers].read_u32(queue::READ)?!=u32::from(head) {return Ok(false);} - } Ok(true) + } + if self.profile==super::m3_profile::Profile::Extended && self.draw!=0 { + for pool in UMA_POOLS { + if self.objects[pool.manager].read_u64(0x54)?!=self.draw {return Ok(false);} + } + } + Ok(true) } pub(crate) fn log(&mut self,dev:&driver::AsahiDrmDevice)->Result { // Retain allocator state as well as payloads: an exhausted parameter diff --git a/drivers/gpu/drm/asahi/m3/m3_render_sequence.rs b/drivers/gpu/drm/asahi/m3/m3_render_sequence.rs index f9a0483e77b21b..3d421e99066929 100644 --- a/drivers/gpu/drm/asahi/m3/m3_render_sequence.rs +++ b/drivers/gpu/drm/asahi/m3/m3_render_sequence.rs @@ -184,3 +184,171 @@ impl Fragment { Ok(if timed {LENGTH} else {without_timestamps(bytes,TIMESTAMP_START,FINALIZE,LENGTH,0xa4)}) } } + +impl Owners { + pub(crate) fn encode_extended(&self, fragment:bool, + auxiliary:super::m3_compute_layout::GpuVa, serial:u64, output:&mut[u8]) -> Result { + if self.context>=64 || serial==0 || self.stamp==0 {return Err(Error::Address);} + let command=self.command.at(0,if fragment {0xca0}else{0x940})?; + let sequence=self.sequence.at(0,STORAGE)?; + let activity=self.stats.at(0,8)?; + let scene_tail=self.scene.get().checked_add(0x54).ok_or(Error::Address)?; + let (clock,pair,aux_time,elapsed)=if fragment {(0xc08,0xc10,0xc70,0xb84)}else{(0x8c0,0x8c8,0x928,0x83c)}; + let first=if fragment {0x1e4}else{0x1cc}; + let finalize=if fragment {0x280}else{0x268}; + let pool=if fragment {0x1b0}else{0x190}; + let length=if fragment {0x370}else{0x300}; + let uuid=if fragment {fragment::UUID}else{ta::UUID}; + let bytes=output.get_mut(..STORAGE).ok_or(Error::Size)?; + bytes.fill(0); + word(bytes,0,if fragment {7}else{5}); + pointer(bytes,0x14,command+if fragment {0x80}else{0x40}); + pointer(bytes,pool,self.pool.get()); + pointer(bytes,pool+0x18,auxiliary.get()); + pointer(bytes,pool+0x28,serial); + word(bytes,pool+0x30,u32::from(fragment)); + if fragment { + pointer(bytes,0x1c,self.scene.get()); + pointer(bytes,0x24,activity); + pointer(bytes,0x2c,command+0xb70); + pointer(bytes,0x34,scene_tail); + pointer(bytes,0x3c,command+0xbb0); + pointer(bytes,0x44,command+0xbb4); + pointer(bytes,0x4c,self.queue.get()); + pointer(bytes,0x54,command); + word(bytes,0x5c,self.context); + word(bytes,0x60,1); + pointer(bytes,0x7c,command+0xa58); + pointer(bytes,0x84,command+0xbf0); + word(bytes,0xa0,uuid); + } else { + pointer(bytes,0x1c,self.manager.get()); + pointer(bytes,0x24,self.scene.get()); + pointer(bytes,0x2c,activity); + pointer(bytes,0x34,self.queue.get()); + pointer(bytes,0x3c,command+0x85c); + word(bytes,0x44,self.context); + word(bytes,0x48,1); + pointer(bytes,0x64,command+0x784); + pointer(bytes,0x6c,command+0x8a8); + word(bytes,0x7c,uuid); + } + for (at,opcode,selected) in [(first,0x8000_0003,pair),(first+0x50,3,pair+8)] { + word(bytes,at,opcode); + pointer(bytes,at+4,command+clock); + pointer(bytes,at+0xc,command+pair); + pointer(bytes,at+0x14,command+selected); + pointer(bytes,at+0x1c,self.queue.get()); + pointer(bytes,at+0x2c,command+aux_time); + pointer(bytes,at+0x34,command+elapsed); + pointer(bytes,at+0x44,uuid.into()); + } + word(bytes,first+0x4c,1); + word(bytes,finalize,if fragment {8}else{6}); + if fragment { + word(bytes,finalize+4,uuid); + pointer(bytes,finalize+0xc,self.fw_stamp.get()); + word(bytes,finalize+0x14,self.stamp); + pointer(bytes,finalize+0x1c,self.scene.get()); + pointer(bytes,finalize+0x24,self.manager.get()); + word(bytes,finalize+0x2c,1); + pointer(bytes,finalize+0x30,activity); + pointer(bytes,finalize+0x38,command+0xbb0); + pointer(bytes,finalize+0x40,command+0xbb4); + pointer(bytes,finalize+0x48,command+0xb70); + pointer(bytes,finalize+0x50,self.queue.get()); + pointer(bytes,finalize+0x58,command); + word(bytes,finalize+0x60,self.context); + pointer(bytes,finalize+0x64,command+0xa58); + pointer(bytes,finalize+0x98,sequence+pool as u64); + word(bytes,finalize+0xa0,(-(finalize as i32))as u32); + pointer(bytes,finalize+0xa5,command+0xc60); + word(bytes,finalize+0xad,1); + bytes[finalize+0xb1]=1; + // J613 terminates this record after the 16-bit event field. + bytes[finalize+0xb2..finalize+0xb4].copy_from_slice(&1u16.to_le_bytes()); + word(bytes,finalize+0xb4,0x4000_0002); + } else { + pointer(bytes,finalize+4,self.scene.get()); + pointer(bytes,finalize+0xc,self.manager.get()); + pointer(bytes,finalize+0x14,activity); + pointer(bytes,finalize+0x1c,self.queue.get()); + pointer(bytes,finalize+0x24,command+0x85c); + word(bytes,finalize+0x2c,self.context); + pointer(bytes,finalize+0x34,command+0x784); + word(bytes,finalize+0x40,uuid); + pointer(bytes,finalize+0x48,self.fw_stamp.get()); + word(bytes,finalize+0x50,self.stamp); + pointer(bytes,finalize+0x78,sequence+pool as u64); + word(bytes,finalize+0x80,(-(finalize as i32))as u32); + pointer(bytes,finalize+0x85,command+0x918); + bytes[finalize+0x8d]=1; + word(bytes,finalize+0x94,0x4000_0002); + } + Ok(length) + } +} + +#[cfg(test)] +mod native_tests { + use super::*; + #[test] fn j613_fragment_ends_before_the_m4_tail_and_keeps_relative_restart() { + let fw=|index:u64|FirmwareVa::new(0xffff_fc25_2000_0000+index*0x4000).unwrap(); + let o=Owners {command:Region::new(fw(0).get(),0xca0).unwrap(),sequence:Region::new(fw(1).get(),STORAGE).unwrap(), + stats:Region::new(fw(2).get(),0x1000).unwrap(),notifier:Region::new(fw(3).get(),0x200).unwrap(), + queue:fw(4),scene:fw(5),manager:fw(6),pool:fw(7),fw_stamp:fw(8),context:3,stamp:0x3d000100}; + let mut output=[0xa5;STORAGE+8]; + let length=o.encode_extended(true,super::super::m3_compute_layout::GpuVa::new(0x72_3000_0200).unwrap(),1,&mut output).unwrap(); + assert_eq!(length,0x370); + let word=|at|u32::from_le_bytes(output[at..at+4].try_into().unwrap()); + assert_eq!(word(0x1e4),0x8000_0003); + assert_eq!(word(0x230),1); + assert_eq!(word(0x234),3); + assert_eq!(word(0x280),8); + assert_eq!(word(0x320),(-0x280i32)as u32); + assert_eq!(word(0x334),0x4000_0002); + assert_eq!(&output[STORAGE..],&[0xa5;8]); + } +} + +/// Native start records reserve one packed attachment list per engine. The +/// matching finalizer flag enables completion visibility for that same list. +pub(crate) const fn extended_attachment_offsets(fragment:bool)->(usize,usize) { + if fragment {(0xa8,0x324)} else {(0x88,0x2ec)} +} + +/// Send both pieces through the same bounded writer used for firmware storage. +#[cfg(any(test, CONFIG_DRM_ASAHI_M3_T8122))] +pub(crate) fn patch_extended_attachments(fragment:bool, + attachments:&super::agx_attachments::Attachments, + mut write:impl FnMut(usize,&[u8])->Result<(),E>)->Result<(),E> { + let (start,flag)=extended_attachment_offsets(fragment); + write(start,&attachments.0)?; + write(flag,&[u8::from(attachments.count()!=0)]) +} + +#[cfg(test)] +mod attachment_tests { + use super::*; + use super::super::agx_attachments::{Attachments,SIZE}; + #[test] fn each_engine_keeps_its_list_and_can_clear_a_previous_list() { + for (fragment,start,flag,pool) in [(false,0x88,0x2ec,0x190),(true,0xa8,0x324,0x1b0)] { + let mut bytes=[0xa5;STORAGE]; + let list=Attachments::new(&[(0x100_0040,0x180),(0x200_0000,0x4000)]).unwrap(); + let mut apply=|list:&Attachments|patch_extended_attachments(fragment,list,|at,data|{ + bytes.get_mut(at..at+data.len()).ok_or(Error::Size)?.copy_from_slice(data);Ok::<(),Error>(()) + }); + apply(&list).unwrap(); + assert_eq!(&bytes[start..start+SIZE],&list.0); + assert_eq!(bytes[flag],1); + assert_eq!(&bytes[pool..pool+8],&[0xa5;8]); + patch_extended_attachments(fragment,&Attachments::EMPTY,|at,data|{ + bytes[at..at+data.len()].copy_from_slice(data);Ok::<(),Error>(()) + }).unwrap(); + assert!(bytes[start..start+SIZE].iter().all(|b|*b==0)); + assert_eq!(bytes[flag],0); + assert_eq!(bytes[start-1],0xa5); + assert_eq!(bytes[start+SIZE],0xa5); + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_runtime.rs b/drivers/gpu/drm/asahi/m3/m3_runtime.rs index 1a366d7f4b3407..3b1f3d6d085356 100644 --- a/drivers/gpu/drm/asahi/m3/m3_runtime.rs +++ b/drivers/gpu/drm/asahi/m3/m3_runtime.rs @@ -389,8 +389,8 @@ impl Inner { let index=found.unwrap_or(self.jobs.len()); if found.is_none() { let job=match control { - crate::m3::m3_submit::Command::Compute(c)=>NativeJob::Compute(crate::m3::m3_compute::Compute::new(&self.drm,&self.uat,&packet.vm,self.config.stats_region().map_err(early)?,c,self.device.soc().registers).map_err(early)?), - crate::m3::m3_submit::Command::Render{..}=>NativeJob::Render(crate::m3::m3_render::Render::new(&self.drm,&self.uat,&packet.vm,self.config.stats_region().map_err(early)?,self.device.soc().clusters,self.device.soc().registers).map_err(early)?), + crate::m3::m3_submit::Command::Compute(c)=>NativeJob::Compute(crate::m3::m3_compute::Compute::new(&self.drm,&self.uat,&packet.vm,self.config.stats_region().map_err(early)?,c,self.device.soc().registers,self.device.firmware().profile()).map_err(early)?), + crate::m3::m3_submit::Command::Render{..}=>NativeJob::Render(crate::m3::m3_render::Render::new(&self.drm,&self.uat,&packet.vm,self.config.render_stats().map_err(early)?,self.device.soc().clusters,self.device.soc().registers,self.device.firmware().profile()).map_err(early)?), }; self.jobs.push(job,GFP_KERNEL).map_err(|_|early(ENOMEM))?; } else { @@ -439,6 +439,7 @@ impl Inner { let timed=timestamps.map(|pair|policy::stage_timestamps(internal_timestamps,chip,pair)); j.append(command,usc,coalesce,timed)?; j.set_user_timestamps(timestamps)?; + j.set_attachments(&p.render_attachments[*next])?; coalesced_stamps+=u64::from(coalesce); } Ok(()) @@ -469,6 +470,9 @@ impl Inner { crate::mem::sync(); self.t8122_verdict=false; let previous_events=self.config.completed_events; + if self.device.firmware().profile() == super::m3_profile::Profile::Extended { + packet.vm.mark_binding_published().map_err(early)?; + } let publication_tick=if crate::debug::debug_enabled(crate::debug::DebugFlags::M3PassTiming) {physical_counter()} else {0}; self.state.health.set_gpu_pending(true); self.gpu_pending = true; @@ -535,7 +539,7 @@ impl Inner { let control=first_compute.ok_or(EINVAL)?; if existing.is_none() { let job=crate::m3::m3_compute::Compute::new(&self.drm,&self.uat,&packet.vm, - self.config.stats_region()?,control,self.device.soc().registers)?; + self.config.stats_region()?,control,self.device.soc().registers,self.device.firmware().profile())?; self.jobs.push(NativeJob::Compute(job),GFP_KERNEL)?; } else { let NativeJob::Compute(j)=&mut self.jobs[compute_index] else {return Err(EINVAL);}; @@ -560,6 +564,7 @@ impl Inner { let timed=timestamps.map(|pair|policy::stage_timestamps(internal_timestamps,self.device.soc().chip_id,pair)); j.append_after(command,usc,false,predecessor,timed)?; j.set_user_timestamps(timestamps)?; + j.set_attachments(&p.render_attachments[*i])?; dependencies+=usize::from(predecessor.is_some()); previous=Some((true,j.completion_dependency()?)); }, @@ -1059,7 +1064,7 @@ impl Runtime { // TTBAT of the admitted reservations, the firmware kernel root (`pagetables`) as // every context's upper half, and the host window (root entry 2) through the // shared L2. The RTKit is awake and no initdata or GPU job has been published. - let (uat,window)=adopted.insert(adopt_uat(&drm,device.soc(),&device.firmware().resources)?); + let (uat,window)=adopted.insert(adopt_uat(&drm,device.soc(),&device.firmware().resources,device.firmware().profile())?); let config=Config::new(&drm,uat,device.firmware(),&contents)?; // Link every kernel table the configuration created before the firmware sees an // address in it, then drop any translation cached before they existed. @@ -1103,7 +1108,8 @@ impl Runtime { // (7.1.12's M3 VM job lifetime, the shared `mmu::lifetime` on 7.2). let vm=self.inner.uat.new_vm(id, range)?.with_tracked_status()?; let mut reserved=KVec::new();reserved.push(reserved_range,GFP_KERNEL)?;reserved.push(0x100_8000_0000..0x101_0000_0000,GFP_KERNEL)?;reserved.push(0x10_0000_0000..0x10_0800_0000,GFP_KERNEL)?;reserved.push(0x10_7400_0000..0x10_7402_8000,GFP_KERNEL)?; - vm.install_driver_mappings(KVec::new(),reserved)?; + let mappings = self.inner.config.global_table_views(&vm)?; + vm.install_driver_mappings(mappings,reserved)?; Ok(vm) } pub(crate) fn map_timestamp(&mut self,mut bo:crate::gem::ObjectRef,range:core::ops::Range)->Result { @@ -1214,6 +1220,9 @@ impl Runtime { } fn boot_inner(&mut self, pdev: &platform::Device>) -> Result { use crate::m3::t8122_start::BootStep; + if self.inner.device.firmware().profile() == super::m3_profile::Profile::Extended { + return self.boot_extended(pdev); + } let root = self.inner.config.root(); dev_info!(pdev.as_ref(), "M3: publishing owned initdata {:#x}\n", root); self.inner.boot_step = BootStep::Publish; @@ -1245,6 +1254,60 @@ impl Runtime { dev_info!(pdev.as_ref(), "M3: firmware accepted owned initdata and device controls\n"); Ok(()) } + fn boot_extended(&mut self, pdev: &platform::Device>) -> Result { + use super::m3_channel_format::Action; + use crate::m3::t8122_start::BootStep; + // The first control is pending when the root becomes visible. Its + // retirement comes from InitData consumption, without a work kick. + let first = self.inner.config.control_extended(Action::Initialize)?; + self.inner.config.publish_io_table(pdev, self.inner.device.firmware())?; + let root = self.inner.config.root(); + Pin::new(&mut self.inner.transport).send_message(0x20, + 0x0081_0000_0000_0000 | (root & ((1u64 << 44) - 1)))?; + self.inner.boot_step = BootStep::AwaitReady; + let deadline = Instant::::now(); + while !self.inner.config.ready()? { + let inner: &mut Inner = &mut *self.inner; + inner.config.drain(&inner.drm)?; + if !inner.state.healthy() { return Err(EIO); } + if deadline.elapsed() >= Delta::from_secs(2) { return Err(ETIMEDOUT); } + fsleep(Delta::from_millis(1)); + } + self.retire_extended_control(first)?; + self.inner.boot_step = BootStep::DeviceControl; + // Each data master registers its two firmware engine classes. + for ordinal in 0..6 { + let action = Action::Register { pipe: ordinal / 2, engine: ordinal % 2 + 1 }; + self.send_extended_control(action)?; + } + self.inner.boot_step = BootStep::PostReady; + self.send_extended_control(Action::Idle { enabled: true })?; + let inner: &mut Inner = &mut *self.inner; + inner.config.check_pstate(&inner.drm, &inner.device, "after native boot")?; + inner.config.log_ready(&inner.drm)?; + inner.state.health.set_gpu_pending(false); + dev_info!(pdev.as_ref(), "M3: extended firmware ready; native controllers and idle policy admitted\n"); + Ok(()) + } + + fn send_extended_control(&mut self, action: super::m3_channel_format::Action) -> Result { + let cursor = self.inner.config.control_extended(action)?; + Pin::new(&mut self.inner.transport).send_message(0x21, 0x0084_0000_0000_0011)?; + self.retire_extended_control(cursor) + } + + fn retire_extended_control(&mut self, expected: u32) -> Result { + let deadline = Instant::::now(); + loop { + let inner: &mut Inner = &mut *self.inner; + inner.config.drain(&inner.drm)?; + if !inner.state.healthy() { return Err(EIO); } + if inner.config.control_done(expected)? { return Ok(()); } + if deadline.elapsed() >= Delta::from_secs(2) { return Err(ETIMEDOUT); } + fsleep(Delta::from_millis(1)); + } + } + fn send_control(&mut self, opcode: u32) -> Result { let next = match self.inner.config.control(opcode) { Ok(next) => next, @@ -1312,13 +1375,15 @@ pub(crate) fn physical_counter() -> u64 { /// (ttbs, pagetables, handoff, shared-l2) and the PPL exchange with its startup epoch, as the /// G15 backend does it on 7.2 (`g15::uat`). fn adopt_uat(drm: &driver::AsahiDrmDevice, soc: &'static crate::m3::m3_soc::Soc, - resources: &crate::m3::agx_resources::Resources) -> Result<(mmu::Uat, crate::g15::uat::Window)> { + resources: &crate::m3::agx_resources::Resources, profile: super::m3_profile::Profile) + -> Result<(mmu::Uat, crate::g15::uat::Window)> { let cfg = soc.hwcfg.ok_or(ENODEV)?; let [ttbs, tables, handoff, shared_l2, ..] = resources.regions; let range = |r: crate::m3::agx_resources::Region| r.base..r.base + r.size; let geometry = mmu::UatGeometry { ias: cfg.uat_ias.into(), oas: cfg.uat_oas }; let regions = mmu::AdoptedRegions { handoff: range(handoff), ttbs: range(ttbs), kernel_root: tables.base }; - let uat = mmu::Uat::new_adopted(drm, geometry, ®ions)?; + let uat = mmu::Uat::new_adopted_with_user_upper(drm, geometry, ®ions, + profile == super::m3_profile::Profile::Legacy)?; let mut window = crate::g15::uat::Window::from_regions(handoff.base, ttbs.base, tables.base, shared_l2.base)?; window.bootstrap(drm.as_ref().as_ref(), &uat)?; // 7.1.12 cleaned every reserved-table write to the point of coherency; these regions may be diff --git a/drivers/gpu/drm/asahi/m3/m3_scene_layout.rs b/drivers/gpu/drm/asahi/m3/m3_scene_layout.rs index 880cf4c2b9a75d..ea3c2064ef39e6 100644 --- a/drivers/gpu/drm/asahi/m3/m3_scene_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_scene_layout.rs @@ -39,3 +39,43 @@ impl Scene { Ok(()) } } + +pub(crate) const EXTENDED_SIZE: usize = 0x400; + +/// Encode a native scene header from its retained aliases. The local +/// statistics and parameter scratch remain part of this same owned record. +pub(crate) fn encode_extended(output: &mut [u8], owner: super::m3_init_layout::Region, + scratch_gpu: super::m3_pool_layout::GpuRegion, scratch_fw: FirmwareVa, + scene_list: super::m3_pool_layout::GpuRegion) -> Result<(), Error> { + let statistics = owner.at(0x200, 0x40).map_err(|_| Error::Size)?; + owner.at(0, EXTENDED_SIZE).map_err(|_| Error::Size)?; + if scratch_gpu.size() < 0x4000 || scene_list.size() < 0x4000 { return Err(Error::Size); } + let compact = super::agx_render_state::compact(scene_list.base()).map_err(|_| Error::Address)?; + let bytes = output.get_mut(..EXTENDED_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + bytes[..8].copy_from_slice(&scratch_gpu.base().to_le_bytes()); + bytes[8..16].copy_from_slice(&scratch_fw.get().to_le_bytes()); + bytes[0x28..0x30].copy_from_slice(&compact.to_le_bytes()); + bytes[0x40..0x48].copy_from_slice(&statistics.to_le_bytes()); + Ok(()) +} + +#[cfg(test)] +mod extended_tests { + use super::*; + use super::super::{m3_init_layout::Region, m3_pool_layout::GpuRegion}; + #[test] fn native_scene_keeps_scratch_aliases_and_local_statistics_distinct() { + let owner = Region::new(0xffff_fc25_0010_0000, EXTENDED_SIZE).unwrap(); + let scratch = GpuRegion::new(0x10_0200_0000, 0x4000).unwrap(); + let scratch_fw = FirmwareVa::new(0xffff_fc25_0020_0000).unwrap(); + let list = GpuRegion::new(0x10_0300_0000, 0x4000).unwrap(); + let mut bytes = [0xa5; EXTENDED_SIZE + 8]; + encode_extended(&mut bytes, owner, scratch, scratch_fw, list).unwrap(); + let read = |at| u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap()); + assert_eq!(read(0), scratch.base()); + assert_eq!(read(8), scratch_fw.get()); + assert_eq!(read(0x28), 0x300_0000); + assert_eq!(read(0x40), owner.at(0x200, 0x40).unwrap()); + assert_eq!(&bytes[EXTENDED_SIZE..], &[0xa5; 8]); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_shared_layout.rs b/drivers/gpu/drm/asahi/m3/m3_shared_layout.rs index 9b8d503836d0a7..a18197890a1ff1 100644 --- a/drivers/gpu/drm/asahi/m3/m3_shared_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_shared_layout.rs @@ -100,3 +100,26 @@ pub(crate) fn fault_buffers()->impl Iterator { .chain((PB_FIRST..PB_FIRST+PB_GROUPS).map(|i|(i,256))) .chain([(TILEMAP_LABEL,1280),(HEAP_METADATA_LABEL,256)]) } + +pub(crate) fn allocation_for(profile: super::m3_profile::Profile, index: usize) -> Result { + let mut owner = allocation(index)?; + if profile == super::m3_profile::Profile::Legacy { return Ok(owner); } + if owner.space == Space::Firmware { + owner.address = 0xffff_fc24_0000_0000 + index as u64 * 0x10_0000; + } + match index { + 8 | 20 => owner.size = 0x20000, + 9 | 21 => { owner.address &= !0x3fff; owner.size = 0x4000; } + 10 | 22 => { + owner.size = 0x4000; + owner.gpu_alias = Some(0x72_3000_0000 + u64::from(index == 22) * 0x4000); + } + GPU_CONTEXT => owner.size = 0x38, + NOTIFIER => owner.size = 0x400, + EVENT_COUNT => owner.size = 16, + BUFFER_MANAGER => owner.size = 0xc0, + FRAGMENT_QUEUE | TA_QUEUE => owner.size = 0x24c0, + _ => {} + } + Ok(owner) +} diff --git a/drivers/gpu/drm/asahi/m3/m3_soc.rs b/drivers/gpu/drm/asahi/m3/m3_soc.rs index 4b81475612b6fa..7500822c4124c5 100644 --- a/drivers/gpu/drm/asahi/m3/m3_soc.rs +++ b/drivers/gpu/drm/asahi/m3/m3_soc.rs @@ -291,6 +291,7 @@ pub(crate) static T8122: Soc = Soc { cores_per_cluster: 10, mailbox_compatibles: &[ b"apple,t8122-agx-asc-mailbox\0", + b"apple,t8122-agx-asc-mailbox\0apple,asc-mailbox-v4\0", b"apple,t8122-asc-mailbox\0apple,asc-mailbox-v4\0", ], mailbox_interrupts: &[0, 723, 4, 0, 724, 4, 0, 725, 4, 0, 726, 4], diff --git a/drivers/gpu/drm/asahi/m3/m3_state_layout.rs b/drivers/gpu/drm/asahi/m3/m3_state_layout.rs index 199b7082b26c29..6eeafea666e742 100644 --- a/drivers/gpu/drm/asahi/m3/m3_state_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_state_layout.rs @@ -66,3 +66,55 @@ pub(crate) fn manager_misc(out: &mut [u8]) -> Result<(), Error> { word(bytes, 0x20, 1); // cpu_flag / reset Ok(()) } + +pub(crate) const EXTENDED_CONTEXT_SIZE: usize = 0x38; +pub(crate) const EXTENDED_NOTIFIER_SIZE: usize = 0x200; + +pub(crate) fn context_extended(output: &mut [u8]) -> Result<(), Error> { + let bytes = output.get_mut(..EXTENDED_CONTEXT_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + bytes[..2].fill(0xff); + bytes[5] = 1; + bytes[0x26] = 2; + bytes[0x33] = 0xff; + Ok(()) +} + +impl Notifier { + pub(crate) fn encode_extended(&self, output: &mut [u8]) -> Result<(), Error> { + if self.context >= 64 { return Err(Error::Address); } + let bytes = output.get_mut(..EXTENDED_NOTIFIER_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + bytes[..8].copy_from_slice(&self.threshold.get().to_le_bytes()); + word(bytes, 0x10, 0x50); + word(bytes, NOTIFIER_CONTEXT, self.context); + bytes[0xa8..0xb0].fill(0xff); + word(bytes, 0x114, 1); + Ok(()) + } +} + +#[cfg(test)] +mod extended_tests { + use super::*; + #[test] fn registration_state_is_left_to_firmware() { + let mut bytes = [0xa5; EXTENDED_CONTEXT_SIZE + 8]; + context_extended(&mut bytes).unwrap(); + assert_eq!(&bytes[..6], &[255, 255, 0, 0, 0, 1]); + assert_eq!(bytes[0x26], 2); + assert_eq!(bytes[0x33], 255); + assert_eq!(&bytes[EXTENDED_CONTEXT_SIZE..], &[0xa5; 8]); + } + #[test] fn notifier_has_zero_generation_and_distinct_firmware_storage() { + let notifier = Notifier { threshold: FirmwareVa::new(0xffff_fc21_7000_0000).unwrap(), context: 3 }; + let mut bytes = [0xa5; EXTENDED_NOTIFIER_SIZE + 8]; + notifier.encode_extended(&mut bytes).unwrap(); + assert_eq!(&bytes[8..12], &[0; 4]); + assert_eq!(&bytes[0xa8..0xb0], &[255; 8]); + assert_eq!(&bytes[0x114..0x118], &[1, 0, 0, 0]); + assert_eq!(&bytes[EXTENDED_NOTIFIER_SIZE..], &[0xa5; 8]); + let mut short = [0xa5; NOTIFIER_SIZE]; + assert!(notifier.encode_extended(&mut short).is_err()); + assert_eq!(short, [0xa5; NOTIFIER_SIZE]); + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_submit.rs b/drivers/gpu/drm/asahi/m3/m3_submit.rs index 1212113af1618a..155bda714e9d9f 100644 --- a/drivers/gpu/drm/asahi/m3/m3_submit.rs +++ b/drivers/gpu/drm/asahi/m3/m3_submit.rs @@ -42,6 +42,7 @@ pub(crate) struct Packet { timestamps:KVec<[Option;4]>, pub(crate) attachments:KVec, pub(crate) wide_visibility:KVec, + pub(crate) render_attachments:KVec<[crate::m3::agx_attachments::Attachments;2]>, completion:UserFence, finish_claimed: AtomicBool, /// Set before a batch with any of this packet's commands can change firmware-visible state, @@ -177,11 +178,12 @@ pub(crate) struct Queue { shared:Shared,vm:mmu::Vm,usc:u64,fences:FenceContexts, /// Whether the SoC advertises early tiling (`m3_soc::Features::fragment_dependency`). early_tiling:bool, + wide_visibility:bool, urgency:u8, } impl Queue { pub(crate) fn new(shared:Shared,scheduler:Arc>,vm:mmu::Vm,priority:u32,usc:u64, - early_tiling:bool)->Result { + early_tiling:bool, wide_visibility:bool)->Result { agx_uapi::QueueUscWindow{base:usc,user_start:0x4000,user_end:(1u64<<42)-0x8000}.validate().map_err(|_|EINVAL)?; // file.rs passes REALTIME - UAPI priority: 3 is UAPI LOW, 0 is UAPI REALTIME. let urgency=3u8.saturating_sub(priority.min(3) as u8); @@ -189,14 +191,14 @@ impl Queue { 1=>sched::Priority::High,0=>sched::Priority::Kernel,_=>return Err(EINVAL)}; Ok(Self {entity:sched::Entity::new(&scheduler,priority)?,_scheduler:scheduler, shared,vm,usc,fences:FenceContexts::new(1,c_str!("asahi_m3_queue"),kernel::static_lock_class!())?, - early_tiling,urgency}) + early_tiling,wide_visibility,urgency}) } } impl queue::Queue for Queue { fn submit(&mut self,_reg_data:&crate::driver::AsahiRegData<'_>,_id:u64,mut syncs:KVec,in_sync_count:usize, raw:&[u8],objects:Pin<&xarray::XArray>>)->Result { let vm_job=self.vm.retain_job()?; - let mut parser=agx_uapi::UapiCommandParser::new(raw);let mut commands=KVec::new();let mut timestamps=KVec::new();let mut flushes=KVec::new();let mut visibility=KVec::new(); + let mut parser=agx_uapi::UapiCommandParser::new(raw);let mut commands=KVec::new();let mut timestamps=KVec::new();let mut flushes=KVec::new();let mut visibility=KVec::new();let mut render_attachments=KVec::new(); while let Some(command)=parser.next_hardware().map_err(|_|EINVAL)? { if let agx_uapi::ParsedHardwareCommand::Render{payload,vertex_attachments,fragment_attachments,..}=command { if let Err(e)=validate_render(payload,self.usc,&AddressSpace(&self.vm),self.early_tiling) { @@ -211,13 +213,15 @@ impl queue::Queue for Queue { Destination::resolve(objects,payload.fragment_timestamps.start)?, Destination::resolve(objects,payload.fragment_timestamps.end)?],GFP_KERNEL)?; flushes.push(crate::m3::agx_attachments::Attachments::EMPTY,GFP_KERNEL)?; + render_attachments.push([encode_attachments(&self.vm,&vertex_attachments)?,encode_attachments(&self.vm,&fragment_attachments)?],GFP_KERNEL)?; visibility.push(false,GFP_KERNEL)?; continue; } let agx_uapi::ParsedHardwareCommand::Compute{mut payload,attachments,..}=command else {return Err(ENOTSUPP);}; // Other engines and helpers need their own M3 firmware translation. let wide=kernel::uapi::drm_asahi_compute_flags_DRM_ASAHI_COMPUTE_WIDE_VISIBILITY as u32; - if payload.flags & !wide !=0 || payload.sampler_count!=0 || payload.sampler_heap!=0 { + if payload.flags & !wide !=0 || (!self.wide_visibility && payload.flags & wide != 0) + || payload.sampler_count!=0 || payload.sampler_heap!=0 { pr_err!("M3 compute rejected: flags={:#x} samplers={} heap={:#x} attachments={} timestamps={}/{}\n",payload.flags,payload.sampler_count,payload.sampler_heap,attachments.as_slice().len(),payload.timestamps.start.handle,payload.timestamps.end.handle); return Err(ENOTSUPP); } @@ -225,6 +229,7 @@ impl queue::Queue for Queue { // The common translator validates the original command/attachment // addresses; this M3 scheduling hint changes none of them. payload.flags &= !wide; + render_attachments.push([crate::m3::agx_attachments::Attachments::EMPTY;2],GFP_KERNEL)?; flushes.push(encode_attachments(&self.vm,&attachments)?,GFP_KERNEL)?; let c=agx_uapi::translate_compute_command(payload,attachments, agx_uapi::QueueUscWindow{base:self.usc,user_start:0x4000,user_end:(1u64<<42)-0x8000}, @@ -238,7 +243,7 @@ impl queue::Queue for Queue { commands.push(Command::Compute(crate::m3::agx_compute::Control{base:c.control_stream_base,end:c.control_stream_end,usc_base:self.usc}),GFP_KERNEL)?; } parser.finish().map_err(|_|EINVAL)?; - let packet=Arc::new(Packet{vm:self.vm.clone(),commands,timestamps,attachments:flushes,wide_visibility:visibility, + let packet=Arc::new(Packet{vm:self.vm.clone(),commands,timestamps,attachments:flushes,wide_visibility:visibility,render_attachments, completion:self.fences.new_fence(0,Completion)?.into(), finish_claimed:AtomicBool::new(false),published:AtomicBool::new(false),urgency:self.urgency,owner:Owner::current(), vm_job:KBox::pin_init(new_mutex!(Some(vm_job)),GFP_KERNEL)?},GFP_KERNEL)?; diff --git a/drivers/gpu/drm/asahi/m3/m3_sync_layout.rs b/drivers/gpu/drm/asahi/m3/m3_sync_layout.rs index 92d533efa82345..2cf5e189140430 100644 --- a/drivers/gpu/drm/asahi/m3/m3_sync_layout.rs +++ b/drivers/gpu/drm/asahi/m3/m3_sync_layout.rs @@ -91,3 +91,58 @@ impl Barrier { Ok(()) } } + +impl InitBufferManager { + /// The extended rebuild path requires a zero staged-block count. Appending + /// already-staged blocks would leave a previous hardware descriptor live. + pub(crate) fn encode_extended(&self, output: &mut [u8]) -> Result<(), Error> { + if self.context >= 64 || self.slot >= 32 { return Err(Error::Address); } + Self { context: self.context, slot: self.slot, block_count: 0, + manager: self.manager, stamp: self.stamp }.encode(output) + } +} + +impl Barrier { + pub(crate) const EXTENDED_SIZE: usize = 0x40; + + pub(crate) fn encode_extended(&self, output: &mut [u8]) -> Result<(), Error> { + if self.event >= 128 || self.wait_value == 0 || self.self_value == 0 { + return Err(Error::Address); + } + let bytes = output.get_mut(..Self::EXTENDED_SIZE).ok_or(Error::Size)?; + bytes.fill(0); + u32_at(bytes, 0, 4); + address_at(bytes, 4, self.stamp); + address_at(bytes, 12, self.stamp); + u32_at(bytes, 0x14, self.wait_value); + u32_at(bytes, 0x20, self.event); + u32_at(bytes, 0x24, self.self_value); + if matches!(self.dependency, Dependency::PreviousPass) { u32_at(bytes, 0x3c, 1); } + Ok(()) + } +} + +#[cfg(test)] +mod extended_tests { + use super::*; + #[test] fn full_rebuild_never_uses_the_legacy_staged_count() { + let mut bytes = [0xa5; INIT_BM_SIZE]; + InitBufferManager { context: 3, slot: 7, block_count: 2048, + manager: FirmwareVa::new(0xffff_fc24_0010_0000).unwrap(), stamp: 0x100 } + .encode_extended(&mut bytes).unwrap(); + assert_eq!(&bytes[12..20], &[0; 8]); + } + #[test] fn extended_dependencies_use_their_native_tail_and_preserve_trailing_storage() { + for previous in [false, true] { + let mut bytes = [0xa5; Barrier::EXTENDED_SIZE + 8]; + Barrier { stamp: FirmwareVa::new(0xffff_fc24_0010_0000).unwrap(), wait_value: 0x100, + event: 2, self_value: 0x200, uuid: 9, + dependency: if previous { Dependency::PreviousPass } else { Dependency::TilerToFragment } } + .encode_extended(&mut bytes).unwrap(); + assert_eq!(&bytes[0x18..0x20], &[0; 8]); + assert_eq!(&bytes[0x20..0x24], &[2, 0, 0, 0]); + assert_eq!(u32::from_le_bytes(bytes[0x3c..0x40].try_into().unwrap()), previous.into()); + assert_eq!(&bytes[Barrier::EXTENDED_SIZE..], &[0xa5; 8]); + } + } +} diff --git a/drivers/gpu/drm/asahi/m3/m3_tiler_command.rs b/drivers/gpu/drm/asahi/m3/m3_tiler_command.rs index eeda857a3ed88a..caa61621e0b049 100644 --- a/drivers/gpu/drm/asahi/m3/m3_tiler_command.rs +++ b/drivers/gpu/drm/asahi/m3/m3_tiler_command.rs @@ -3,6 +3,17 @@ use super::{agx_render::Geometry, m3_compute_layout::{GpuVa,RegisterSet}, m3_init_layout::{Error,Region}, m3_queue_layout::FirmwareVa, m3_render_sequence::ta}; pub(crate) const SIZE:usize=0x92b; +pub(crate) const EXTENDED_SIZE:usize=0x940; +pub(crate) const EXTENDED_FLUSH:usize=0x894; +pub(crate) struct Dispatch26 { + pub(crate) incarnation:u8, + pub(crate) serial:u64, + pub(crate) peer_stamp:u32, + pub(crate) usc_window:u64, + pub(crate) firmware_aux:FirmwareVa, + pub(crate) scene_scratch:GpuVa, +} + pub(crate) const REGISTERS:usize=ta::REGISTERS; /// The registers G15G does not take; the others keep the G15S order. pub(crate) const G15G_OMIT:[u32;14]=[0x1c041,0x1c9d0,0x1c948,0x1c888,0x1c890,0x1c918,0x1c089,0x1c9e0,0x16c41,0x1ca40,0x1c9a8,0x1c920,0xa5a1,0xd419]; @@ -57,17 +68,32 @@ impl Command { /// All address checks precede mutation. Zero all reserved fields, then /// emit semantic registers and explicit packed command/tail fields. pub(crate) fn encode(&self,out:&mut[u8])->Result<(),Error> { - if out.len() Result<(),Error> { + self.encode_format(out, Some(dispatch)) + } + fn encode_format(&self, out: &mut [u8], dispatch: Option) -> Result<(),Error> { + let extended = dispatch.is_some(); + let size = if extended { EXTENDED_SIZE } else { SIZE }; + if out.len()= 1 << 42 || self.register_set != RegisterSet::G15G { + return Err(Error::Address); + } + } if self.context>=64 || self.page_count==0 || self.tpc_bytes==0 {return Err(Error::Address);} let registers=self.gpu_alias.get().checked_add(REGISTERS as u64).ok_or(Error::Address)?; - GpuVa::new(self.gpu_alias.get().checked_add((SIZE-3) as u64).ok_or(Error::Address)?) + GpuVa::new(self.gpu_alias.get().checked_add(((size-1)&!3) as u64).ok_or(Error::Address)?) .map_err(|_|Error::Address)?; - let seq=self.sequence.at(0,ta::LENGTH)?; + let sequence_size = if extended { 0x300 } else { ta::LENGTH }; + let seq=self.sequence.at(0,sequence_size)?; let tilemap=compact(self.tilemap.get())?; let tpc=compact(self.tpc.get())?; let heap=compact(self.heap.get())?; let preemption=[compact(self.preemption[0].get())?,compact(self.preemption[1].get())?,compact(self.preemption[2].get())?]; - let user=compact(self.scene_user.get().checked_add(0x40).ok_or(Error::Address)?)?; + let user=compact(self.scene_user.get().checked_add(if extended {0} else {0x40}).ok_or(Error::Address)?)?; let g=&self.geometry; let layers=g.layers; let layer_metadata=compact(self.heap.get().checked_add(if layers>1 {0x200}else{0}).ok_or(Error::Address)?)?; @@ -99,23 +125,67 @@ impl Command { (0x1c830,0),(0x1ca30,user),(0x16c39,user), (0x1c910,0xa0_00000000|(self.initial_scene_entry.get()&0xf_ffffffff)|1), ]; - let bytes=&mut out[..SIZE];bytes.fill(0); // TA opcode is zero - pointer(bytes,4,1); // qualified work-command counter remains one + let bytes=&mut out[..size];bytes.fill(0); // TA opcode is zero + pointer(bytes,4,dispatch.as_ref().map_or(1, |native| native.serial)); word(bytes,CONTEXT,self.context); - for (offset,value) in [(0x14,self.notifier.get()),(0x24,self.manager.get()), - (0x2c,self.scene.get()),(0x34,self.empty.get()),(0x740,registers), + for (offset,value) in [(if extended {0x10} else {0x14},self.notifier.get()), + (if extended {0x20} else {0x24},self.manager.get()), + (if extended {0x28} else {0x2c},self.scene.get()), + (if extended {0x30} else {0x34},self.empty.get()),(0x740,registers), (0x760,self.tpc.get()),(0x768,self.tpc_bytes),(0x770,seq)] {pointer(bytes,offset,value);} let g15g=self.register_set==RegisterSet::G15G; let mut count=0; - for (register,value) in values.into_iter().filter(|(r,_)| !g15g || !G15G_OMIT.contains(r)) { - let n=count;count+=1; - word(bytes,REGISTERS+n*REGISTER_STRIDE,register); - pointer(bytes,REGISTERS+n*REGISTER_STRIDE+4,value); + let mut emit = |register, value| { + word(bytes, REGISTERS + count * REGISTER_STRIDE, register); + pointer(bytes, REGISTERS + count * REGISTER_STRIDE + 4, value); + count += 1; + }; + for (register, mut value) in values { + if g15g && G15G_OMIT.contains(®ister) { continue; } + if extended && matches!(register, 0x12099 | 0x1c898) { continue; } + if let Some(native) = &dispatch { + if register == 0x101e1 { + emit(0xa309, 0); + emit(0x1c8e0, u64::MAX); + emit(0x1c8e8, u64::MAX); + emit(0x1c898, 0); + } + if register == 0x1c910 { + let address = native.scene_scratch.get(); + let escaped = (address & (1 << 42)) >> 3; + let bias = if escaped == 0 { 0x70_0000_0000 } else { 0 }; + value = ((address + bias) & 0x7f_ffff_fffe) | escaped | 1; + } + } + emit(register, value); + } + if let Some(native) = &dispatch { + emit(0xa5a1, 0x60_0040_0020); + emit(0xd419, 0x2_0000_0001); + for register in [0x1ca10, 0x14a1, 0xa349] { emit(register, ta::UUID.into()); } + emit(0x10061, native.usc_window); } bytes[0x748..0x74a].copy_from_slice(&(count as u16).to_le_bytes()); bytes[0x74a..0x74c].copy_from_slice(&((count*REGISTER_STRIDE) as u16).to_le_bytes()); - word(bytes,SEQUENCE_LENGTH,ta::LENGTH as u32); + word(bytes,SEQUENCE_LENGTH,sequence_size as u32); word(bytes,0x77c,1); // fragment event slot + if let Some(native) = dispatch { + word(bytes, STAMP_VALUE, native.peer_stamp); + pointer(bytes, 0x7a8, preemption[0]); + pointer(bytes, 0x878, self.stamp.get()); + pointer(bytes, 0x880, self.fw_stamp.get()); + word(bytes, 0x888, self.stamp_value); + word(bytes, EXTENDED_FLUSH, 1); + word(bytes, 0x898, ta::UUID); + pointer(bytes, 0x8c8, self.timestamps[0].get()); + pointer(bytes, 0x8d0, self.timestamps[1].get()); + pointer(bytes, 0x8d8, self.user_timestamps[0].get()); + pointer(bytes, 0x8e0, self.user_timestamps[1].get()); + pointer(bytes, 0x8fe, self.pool.get()); + bytes[0x906] = 1; + pointer(bytes, 0x90f, native.firmware_aux.get()); + bytes[0x917] = native.incarnation; + } else { word(bytes,STAMP_VALUE,self.stamp_value); pointer(bytes,ta::STRUCT3+0x24,preemption[0]); word(bytes,ta::STRUCT3+0xbc,0x67); // retained producer encoder_id @@ -132,6 +202,41 @@ impl Command { (0x47,self.scratch.get())] {pointer(bytes,ta::COMMAND_TAIL+offset,value);} bytes[ta::COMMAND_TAIL+0x36]=1; // unk_846 bytes[ta::COMMAND_TAIL+0x4f]=1; // unk_85f + } Ok(()) } } + +#[cfg(test)] +mod extended_tests { + use super::*; + fn command() -> Command { + let fw=|index:u64|FirmwareVa::new(0xffff_fc25_1000_0000+index*0x4000).unwrap(); + let gpu=|index:u64|GpuVa::new(0x10_0100_0000+index*0x4000).unwrap(); + Command {context:3,register_set:RegisterSet::G15G,notifier:fw(0),manager:fw(1),scene:fw(2),empty:fw(3), + gpu_alias:GpuVa::new(0x72_1000_0000).unwrap(),sequence:Region::new(fw(4).get(),0x1000).unwrap(), + pool:fw(5),scratch:fw(6),stamp:fw(7),fw_stamp:fw(8),stamp_value:0x7a000100, + timestamps:[fw(9),fw(10)],user_timestamps:[fw(11),fw(12)],preemption:[gpu(0),gpu(1),gpu(2)], + tilemap:gpu(3),tpc:gpu(4),tpc_bytes:0x4000,heap:gpu(5),scene_user:gpu(6), + initial_scene_entry:fw(13),geometry:Geometry::new(64,64,super::super::agx_render::Utile::Pixels32, + super::super::agx_render::Utile::Pixels32,1).unwrap(),page_count:128, + vdm:0x100000,multisample:0,ppp:0,process_empty_tiles:false} + } + #[test] fn native_tiler_keeps_peer_completion_and_usc_window_explicit() { + let value=command();let mut bytes=[0xa5;EXTENDED_SIZE+8]; + value.encode_extended(Dispatch26 {incarnation:9,serial:2,peer_stamp:0x3d000100, + usc_window:1<<32,firmware_aux:value.scratch, + scene_scratch:GpuVa::new(0x10_0200_0000).unwrap()},&mut bytes).unwrap(); + let word=|at|u32::from_le_bytes(bytes[at..at+4].try_into().unwrap()); + assert_eq!(word(0x780),0x3d000100); + assert_eq!(word(0x888),value.stamp_value); + assert_eq!(word(0x894),1); + assert_eq!(word(0x748)&0xffff,66); + assert_eq!(bytes[0x917],9); + assert_eq!(&bytes[EXTENDED_SIZE..],&[0xa5;8]); + let registers=&bytes[REGISTERS..REGISTERS+66*REGISTER_STRIDE]; + assert!(!registers.chunks_exact(12).any(|r|u32::from_le_bytes(r[..4].try_into().unwrap())==0x12099)); + let usc=registers.chunks_exact(12).find(|r|u32::from_le_bytes(r[..4].try_into().unwrap())==0x10061).unwrap(); + assert_eq!(u64::from_le_bytes(usc[4..].try_into().unwrap()),1<<32); + } +} diff --git a/drivers/gpu/drm/asahi/m3/mod.rs b/drivers/gpu/drm/asahi/m3/mod.rs index 1d10657c02f662..d885da1a8647ca 100644 --- a/drivers/gpu/drm/asahi/m3/mod.rs +++ b/drivers/gpu/drm/asahi/m3/mod.rs @@ -198,3 +198,12 @@ mod t8122_admission; pub(crate) mod m3_soc; mod t8122_hwdata; + +mod m3_init_26; +mod m3_profile; +mod m3_mmio_26; + +#[cfg(CONFIG_DRM_ASAHI_M3_T8122)] +mod m3_power_26; + +mod m3_channel_format; diff --git a/drivers/gpu/drm/asahi/m3/t8122_admission.rs b/drivers/gpu/drm/asahi/m3/t8122_admission.rs index 7505065810acc3..65a1df4bc0a6b6 100644 --- a/drivers/gpu/drm/asahi/m3/t8122_admission.rs +++ b/drivers/gpu/drm/asahi/m3/t8122_admission.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT -//! Read-only T8122 identity admission. Firmware startup remains disabled. +//! Read-only T8122 identity and firmware-interface admission. + +use super::m3_profile::Profile; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub(crate) struct Identity { @@ -16,12 +18,23 @@ pub(crate) fn validate_identity( result: u32, id_version: u32, masks: [u32; 3], +) -> Option { + validate_identity_for(Profile::Legacy, firmware_compat, schema, result, id_version, masks) +} + +pub(crate) fn validate_identity_for( + profile: Profile, + firmware_compat: &[u32], + schema: u32, + result: u32, + id_version: u32, + masks: [u32; 3], ) -> Option { let family = id_version >> 24; let variant = (id_version >> 16) & 0xff; let revision = (id_version >> 8) & 0xff; let active_cores = masks[0].count_ones(); - if firmware_compat != [14, 8, 3] + if firmware_compat != profile.firmware_compat() || schema != 1 || result != 1 || family != 7 @@ -40,6 +53,28 @@ pub(crate) fn validate_identity( }) } +/// Firmware-interface selection is exact and does not change physical GPU encodings. +pub(crate) fn profile_for(compat:&[u32],j613:bool)->Option { + match compat { + [14,8,3]=>Some(Profile::Legacy), + [26,6,2] if j613=>Some(Profile::Extended), + _=>None, + } +} + +#[cfg(not(test))] +pub(crate) fn loaded_profile(pdev:&kernel::platform::Device>) + ->kernel::error::Result { + use kernel::{c_str,prelude::*}; + let node=pdev.as_ref().of_node().ok_or(ENODEV)?; + let compat:KVec=node.get_property(c_str!("apple,firmware-compat"))?; + #[cfg(CONFIG_DRM_ASAHI_M3_T8122)] + let j613 = super::m3_board::board_is(b"apple,j613"); + #[cfg(not(CONFIG_DRM_ASAHI_M3_T8122))] + let j613 = false; + profile_for(&compat, j613).ok_or(ENODEV) +} + /// Shape of the voltage-sorted running-state tables: one entry per distinct millivolt, /// and the highest primary frequency in MHz. Voltages round up, matching `PwrConfig`. /// The first point is the off state. Both primary and secondary frequencies must increase @@ -94,6 +129,14 @@ pub(crate) fn opp_table_shape(opps: impl IntoIterator) -> Opt #[cfg(not(test))] pub(crate) fn read_identity( pdev: &kernel::platform::Device>, +) -> kernel::error::Result { + read_identity_for(pdev, loaded_profile(pdev)?) +} + +#[cfg(not(test))] +pub(crate) fn read_identity_for( + pdev: &kernel::platform::Device>, + profile: Profile, ) -> kernel::error::Result { use kernel::{bindings, c_str, prelude::*}; @@ -146,7 +189,8 @@ pub(crate) fn read_identity( observation.get_u32(c_str!("core-mask-1"))?, observation.get_u32(c_str!("core-mask-2"))?, ]; - validate_identity(firmware_compat.as_slice(), schema, result, id_version, masks).ok_or(ENODEV) + validate_identity_for(profile, firmware_compat.as_slice(), schema, result, id_version, masks) + .ok_or(ENODEV) } #[cfg(test)] @@ -164,6 +208,29 @@ mod tests { } } + #[test] + fn extended_identity_requires_its_actual_version_and_the_same_g15g_bounds() { + for mask in [0xff, 0x3ff] { + let good = || validate_identity_for(Profile::Extended, &[26, 6, 2], 1, 1, G15G_C0, [mask, 0, 0]); + assert_eq!(good().unwrap().active_cores, mask.count_ones()); + assert!(validate_identity(&[26, 6, 2], 1, 1, G15G_C0, [mask, 0, 0]).is_none()); + assert!(validate_identity_for(Profile::Extended, &[14, 8, 3], 1, 1, G15G_C0, [mask, 0, 0]).is_none()); + } + for (schema, result, id, masks) in [ + (0, 1, G15G_C0, [0xff, 0, 0]), + (1, 0, G15G_C0, [0xff, 0, 0]), + (1, 1, G15G_C0 ^ (1 << 24), [0xff, 0, 0]), + (1, 1, G15G_C0 ^ (1 << 16), [0xff, 0, 0]), + (1, 1, G15G_C0 ^ (1 << 8), [0xff, 0, 0]), + (1, 1, G15G_C0, [0x1ff, 0, 0]), + (1, 1, G15G_C0, [0x7ff, 0, 0]), + (1, 1, G15G_C0, [0xff, 1, 0]), + (1, 1, G15G_C0, [0xff, 0, 1]), + ] { + assert!(validate_identity_for(Profile::Extended, &[26, 6, 2], schema, result, id, masks).is_none()); + } + } + /// The booted J613 ladder (`perf-states`, frequency then millivolts), in ADT order: fourteen /// states, eight voltages above the idle one, 1338 MHz at the top. const J613_PERF_STATES: [(u64, u32); 14] = [ @@ -255,3 +322,17 @@ mod tests { assert!(validate_identity(&[14, 8, 3], 1, 1, G15G_C0, [0x7f, 0, 0]).is_none()); } } + +#[cfg(test)] +mod selection_tests { + use super::*; + #[test] fn extended_admission_requires_exact_firmware_and_j613() { + for board in [false,true] { + assert_eq!(profile_for(&[14,8,3],board),Some(Profile::Legacy)); + assert_eq!(profile_for(&[26,6,2],board),if board{Some(Profile::Extended)}else{None}); + for wrong in [&[][..],&[26,6][..],&[26,6,1][..],&[26,6,2,0][..],&[14,8,4][..]] { + assert_eq!(profile_for(wrong,board),None); + } + } + } +} diff --git a/drivers/gpu/drm/asahi/mmu.rs b/drivers/gpu/drm/asahi/mmu.rs index ed032ab146dd06..faf1f086fd11c3 100644 --- a/drivers/gpu/drm/asahi/mmu.rs +++ b/drivers/gpu/drm/asahi/mmu.rs @@ -1156,8 +1156,7 @@ impl VmBind { self.1 } - /// Stable wire incarnation for this active root lease. M4 reserves zero as unbound. - #[cfg(CONFIG_DRM_ASAHI_G16)] + /// Stable firmware wire incarnation for this active root lease; zero is unbound. pub(crate) fn generation(&self) -> u8 { self.0.binding.lock().generation } @@ -2670,7 +2669,6 @@ impl Vm { } /// Spend the binding generation only for an incarnation exposed to firmware. - #[cfg(CONFIG_DRM_ASAHI_G16)] pub(crate) fn mark_binding_published(&self) -> Result { let uat = self.gpuvm.lock_inner().uat_inner.clone(); let binding = self.binding.lock(); diff --git a/drivers/pmdomain/apple/pmp-report.c b/drivers/pmdomain/apple/pmp-report.c index 9aac3ced17cda1..f76c1773432694 100644 --- a/drivers/pmdomain/apple/pmp-report.c +++ b/drivers/pmdomain/apple/pmp-report.c @@ -77,6 +77,8 @@ struct apple_pmp_report { struct dentry *debugfs; /* Set only for a report whose halted PMP this driver starts. */ struct apple_pmp_start *start; + struct device_node *native_pmp; + u64 native_claimed; }; static const struct apple_pmp_report_offsets apple_pmp_offsets_t6030; @@ -175,6 +177,7 @@ static void apple_pmp_report_release(void *data) synchronize_rcu(); /* The debugfs proxy drains active reads and rejects subsequent accesses. */ debugfs_remove(rep->debugfs); + of_node_put(rep->native_pmp); } /* The unique, enabled and acknowledged T6030 display request (DISP, reg 7). */ @@ -449,7 +452,7 @@ static const struct { * records are not in ID order, so the bit is the index of the record that * holds that ID. Only a report that starts its PMP has that table. */ -static int apple_pmp_report_bit(const struct apple_pmp_start *st, +static int apple_pmp_report_record(struct device_node *pmp, struct device_node *entry, u32 *bit, bool *record) { const u8 *table; @@ -463,9 +466,9 @@ static int apple_pmp_report_bit(const struct apple_pmp_start *st, *bit = reg; return 0; } - if (!st) + if (!pmp) return -EINVAL; - table = of_get_property(st->pmp, "apple,tunable-soc-device", &len); + table = of_get_property(pmp, "apple,tunable-soc-device", &len); index = apple_pmp_record_of_id(table, len > 0 ? len : 0, reg + 1); if (index < 0 || index > 63) return -ENOENT; @@ -473,6 +476,73 @@ static int apple_pmp_report_bit(const struct apple_pmp_start *st, return 0; } +static int apple_pmp_report_bit(const struct apple_pmp_start *st, + struct device_node *entry, u32 *bit, bool *record) +{ + return apple_pmp_report_record(st ? st->pmp : NULL, entry, bit, record); +} + +/* The native PMP owns startup; this report still exclusively owns PTD votes. */ +static int apple_pmp_native_records(struct apple_pmp_report *rep) +{ + struct device_node *pmp, *link; + const u8 *table, *group, *ranges; + const char *uuid; + int ntable, ngroup, nranges; + + { + struct device_node *chosen = of_find_node_by_path("/chosen"); + const char *opt = NULL; + int len = 0; + bool selected; + + if (chosen) + opt = of_get_property(chosen, "asahi,j613-25g83-dcpext", &len); + selected = opt && len == 2 && opt[0] == '1' && !opt[1] && + of_device_is_compatible(rep->dev->of_node, "apple,j613-25g83-pmp-report"); + of_node_put(chosen); + if (!selected) + return 0; + } + if (!of_machine_is_compatible("apple,j613") || + !of_device_is_compatible(rep->dev->of_node, "apple,j613-25g83-pmp-report")) + return -ENODEV; + for_each_compatible_node(pmp, NULL, "apple,t8122-pmp-v2") { + link = of_parse_phandle(pmp, "apple,pmp-report", 0); + if (link != rep->dev->of_node || !of_device_is_available(pmp)) { + of_node_put(link); + continue; + } + of_node_put(link); + if (rep->native_pmp) { + of_node_put(pmp); + return -EINVAL; + } + rep->native_pmp = of_node_get(pmp); + } + pmp = rep->native_pmp; + if (!pmp || of_property_read_string(pmp, "apple,tunable-uuid", &uuid) || + strcmp(uuid, "EFD60284-7C58-33A9-8522-CB3366AF6040")) + return -EINVAL; + table = of_get_property(pmp, "apple,tunable-soc-device", &ntable); + group = of_get_property(pmp, "apple,tunable-soc-device-ps-group", &ngroup); + ranges = of_get_property(pmp, "apple,tunable-ptd-range", &nranges); + if (!table || ntable != 20 * PMP_SOC_DEVICE_SIZE || !group || ngroup != 8 || + get_unaligned_le64(group) != 0x3fff0 || + !apple_pmp_ranges_valid(ranges, nranges, 0x1000, 0x10800, 0x1080) || + apple_pmp_record_of_id(table, ntable, 8) != 11 || + apple_pmp_record_of_id(table, ntable, 9) != 12 || + apple_pmp_record_of_id(table, ntable, 18) != 19 || + memcmp(table + 11 * PMP_SOC_DEVICE_SIZE + 0x74, "DISP\0\0\0\0", 8) || + memcmp(table + 12 * PMP_SOC_DEVICE_SIZE + 0x74, "DISPEXT\0", 8) || + memcmp(table + 19 * PMP_SOC_DEVICE_SIZE + 0x74, "ANS\0\0\0\0\0", 8) || + !apple_pmp_records_valid(table, ntable, BIT_ULL(11) | BIT_ULL(12) | BIT_ULL(19), + BIT_ULL(11) | BIT_ULL(12))) + return -EINVAL; + dev_dbg(rep->dev, "native26 PTD admitted: DISP11 DISPEXT12 ANS19 (no ANS ack)\n"); + return 0; +} + /* The single PMGR power state of this SoC with @label. */ static struct device_node *apple_pmp_start_domain(struct apple_pmp_report *rep, const char *label) @@ -1083,6 +1153,17 @@ int apple_pmp_report_wait_ready(struct device_node *entry, unsigned long timeout list_for_each_entry_rcu(rep, &apple_pmp_reports, list) { if (rep->dev->of_node == parent) { bound = true; + if (rep->native_pmp) { + ret = apple_pmp_report_record(rep->native_pmp, entry, &id, &record); + if (!ret && (!record || id != 12)) + ret = -EINVAL; + if (!ret && (!(readq(rep->base + rep->offsets->status) & 1) || + !(readq(rep->base + rep->offsets->actual) & BIT_ULL(id)) || + !(readq(rep->base + rep->offsets->tgt_read) & BIT_ULL(id)))) + ret = -EIO; + rcu_read_unlock(); + goto out; + } break; } } @@ -1122,6 +1203,10 @@ static int apple_pmp_report_probe(struct platform_device *pdev) if (ret) return ret; + ret = apple_pmp_native_records(rep); + if (ret) + return dev_err_probe(dev, ret, "native26 PTD table refused\n"); + /* Before the entries exist: they request by soc-device record through it. */ if (rep->offsets->starts_pmp) { ret = apple_pmp_start_init(rep); @@ -1265,6 +1350,7 @@ struct apple_pmp_report_entry { bool sleep_release; bool released; bool removed; + struct apple_pmp_report *native_owner; }; #define genpd_to_apple_pmp_report_entry(_genpd) \ @@ -1275,6 +1361,7 @@ static int apple_pmp_report_set_state(struct apple_pmp_report *rep, u32 id, { u64 bit_val = BIT_ULL(id); u64 val; + bool latched = true; unsigned long flags; spin_lock_irqsave(&rep->lock, flags); @@ -1283,7 +1370,11 @@ static int apple_pmp_report_set_state(struct apple_pmp_report *rep, u32 id, if (enable) val |= bit_val; writeq(val, rep->base + rep->offsets->tgt_write); + if (rep->native_pmp) + latched = !!(readq(rep->base + rep->offsets->tgt_read) & bit_val) == enable; spin_unlock_irqrestore(&rep->lock, flags); + if (!latched) + return -EIO; val = readq(rep->base + rep->offsets->status); if ((val & PMP_REPORT_READY) == 0 || no_ack) return 0; @@ -1457,6 +1548,14 @@ static int apple_pmp_report_entry_power_off(struct generic_pm_domain *genpd) static void apple_pmp_report_entry_free(struct apple_pmp_report_entry *ent) { + if (ent->native_owner) { + unsigned long flags; + + spin_lock_irqsave(&ent->native_owner->lock, flags); + ent->native_owner->native_claimed &= ~BIT_ULL(ent->id); + spin_unlock_irqrestore(&ent->native_owner->lock, flags); + } + of_node_put(ent->report_node); of_node_put(ent->node); kfree(ent->genpd.name); @@ -1482,7 +1581,7 @@ static int apple_pmp_report_entry_probe(struct platform_device *pdev) struct device *dev = &pdev->dev; struct device_node *node = dev->of_node; struct apple_pmp_report_entry *ent; - const struct apple_pmp_report *rep; + struct apple_pmp_report *rep; struct apple_pmp_start *start; int ret; const char *name; @@ -1507,7 +1606,30 @@ static int apple_pmp_report_entry_probe(struct platform_device *pdev) /* Only a report that starts its PMP requests by record, or without ack. */ rep = dev->parent ? dev_get_drvdata(dev->parent) : NULL; start = rep ? rep->start : NULL; - if (start) { + if (rep && rep->native_pmp) { + unsigned long flags; + bool record; + + ret = apple_pmp_report_record(rep->native_pmp, node, &ent->id, &record); + if (ret || !record || (ent->id != 11 && ent->id != 12 && ent->id != 19)) { + ret = -EINVAL; + goto err_free; + } + ent->no_ack = of_property_read_bool(node, "apple,no-ack"); + if (ent->no_ack != (ent->id == 19)) { + ret = -EINVAL; + goto err_free; + } + spin_lock_irqsave(&rep->lock, flags); + ret = rep->native_claimed & BIT_ULL(ent->id) ? -EBUSY : 0; + if (!ret) { + rep->native_claimed |= BIT_ULL(ent->id); + ent->native_owner = rep; + } + spin_unlock_irqrestore(&rep->lock, flags); + if (ret) + goto err_free; + } else if (start) { bool record; ret = apple_pmp_report_bit(start, node, &ent->id, &record); @@ -1532,13 +1654,21 @@ static int apple_pmp_report_entry_probe(struct platform_device *pdev) if (of_property_read_bool(node, "apple,always-on")) { ent->genpd.flags |= GENPD_FLAG_ACTIVE_WAKEUP; - apple_pmp_report_entry_set_state(&ent->genpd, true); + ret = apple_pmp_report_entry_set_state(&ent->genpd, true); + if (ret && ent->native_owner) + goto err_free; + /* Native DCPEXT keeps its resident producer and request through s2idle. */ + if (ent->native_owner && ent->id == 12) { + ent->genpd.flags |= GENPD_FLAG_ALWAYS_ON; + ent->sleep_release = false; + } } ent->genpd.power_on = apple_pmp_report_entry_power_on; ent->genpd.power_off = apple_pmp_report_entry_power_off; - ret = pm_genpd_init(&ent->genpd, NULL, true); + ret = pm_genpd_init(&ent->genpd, NULL, + !(ent->genpd.flags & GENPD_FLAG_ALWAYS_ON)); if (ret) { ret = dev_err_probe(dev, ret, "pm_genpd_init failed\n"); goto err_free; diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c index 11334b3f68faa5..b9d801ed56979d 100644 --- a/drivers/soc/apple/rtkit.c +++ b/drivers/soc/apple/rtkit.c @@ -6,6 +6,7 @@ #include "rtkit-internal.h" +#include #include #include #include @@ -108,6 +109,7 @@ struct apple_rtkit_rx_work { struct apple_rtkit *rtk; u8 ep; u64 msg; + u64 received_ns; struct work_struct work; }; @@ -832,7 +834,10 @@ static void apple_rtkit_rx_work(struct work_struct *work) apple_rtkit_oslog_rx(rtk, rtk_work->msg); break; case APPLE_RTKIT_APP_ENDPOINT_START ... 0xff: - if (rtk->ops->recv_message) + if (rtk->ops->recv_message_timed) + rtk->ops->recv_message_timed(rtk->cookie, rtk_work->ep, + rtk_work->msg, rtk_work->received_ns); + else if (rtk->ops->recv_message) rtk->ops->recv_message(rtk->cookie, rtk_work->ep, rtk_work->msg); else @@ -889,6 +894,7 @@ static void apple_rtkit_rx(struct apple_mbox *mbox, struct apple_mbox_msg msg, struct apple_rtkit *rtk = cookie; struct apple_rtkit_rx_work *work; u8 ep = msg.msg1; + u64 received_ns = ktime_get_ns(); if (READ_ONCE(rtk->shutting_down)) return; @@ -938,6 +944,7 @@ static void apple_rtkit_rx(struct apple_mbox *mbox, struct apple_mbox_msg msg, work->rtk = rtk; work->ep = ep; work->msg = msg.msg0; + work->received_ns = received_ns; INIT_WORK(&work->work, apple_rtkit_rx_work); queue_work(rtk->wq, &work->work); } diff --git a/include/linux/soc/apple/rtkit.h b/include/linux/soc/apple/rtkit.h index 1ba27a92e2b144..0a2718bc39401d 100644 --- a/include/linux/soc/apple/rtkit.h +++ b/include/linux/soc/apple/rtkit.h @@ -57,6 +57,9 @@ struct apple_rtkit_shmem { * context. * @recv_message: Function called when a message from RTKit is received * on a non-system endpoint. Called from a worker thread. + * @recv_message_timed: Like recv_message, carrying the monotonic nanosecond + * timestamp captured before deferring mailbox RX. If set, + * this callback is used instead of recv_message. * @recv_message_early: * Like recv_message, but called from atomic context. It * should return true if it handled the message. If it @@ -77,6 +80,7 @@ struct apple_rtkit_ops { unsigned int flags; void (*crashed)(void *cookie, const void *crashlog, size_t crashlog_size); void (*recv_message)(void *cookie, u8 endpoint, u64 message); + void (*recv_message_timed)(void *cookie, u8 endpoint, u64 message, u64 received_ns); bool (*recv_message_early)(void *cookie, u8 endpoint, u64 message); int (*shmem_setup)(void *cookie, struct apple_rtkit_shmem *bfr); void (*shmem_destroy)(void *cookie, struct apple_rtkit_shmem *bfr); diff --git a/rust/kernel/soc/apple/rtkit.rs b/rust/kernel/soc/apple/rtkit.rs index 87ec3bd73658bb..2581e44d592fc6 100644 --- a/rust/kernel/soc/apple/rtkit.rs +++ b/rust/kernel/soc/apple/rtkit.rs @@ -224,6 +224,7 @@ impl RtKit { }, crashed: Some(crashed_callback::), recv_message: Some(recv_message_callback::), + recv_message_timed: None, recv_message_early: Some(recv_message_early_callback::), shmem_setup: if T::HAS_SHMEM_ALLOC || T::HAS_SHMEM_MAP { Some(shmem_setup_callback::) diff --git a/tools/asahi/dcpext26-admission-tests.py b/tools/asahi/dcpext26-admission-tests.py new file mode 100644 index 00000000000000..bd862bab759896 --- /dev/null +++ b/tools/asahi/dcpext26-admission-tests.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Actual native identity and live-session predicates; classic stays separate.""" +from pathlib import Path +import os,subprocess,tempfile +ROOT=Path(__file__).resolve().parents[2] +header=(ROOT/'drivers/gpu/drm/apple/j613-25g83.h').read_text() +def function(name): + pos=header.index('static inline bool '+name+'(') + return header[pos:header.index('\n}',pos)+2] +source=r''' +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; +#define BIT(n) (1U<<(n)) +#define J613_25G83_DCP_UUID "C042E95C-B9D8-3F0E-94B3-582A08AA6FDD" +'''+function('j613_25g83_identity')+function('j613_25g83_external_state')+r''' +int main(void) { + u32 version[]={26,6,2},state[]={1,16,34,1}; + assert(j613_25g83_identity(true,1,version,3,J613_25G83_DCP_UUID)); + for(unsigned i=0;i<3;i++) { version[i]++;assert(!j613_25g83_identity(true,1,version,3,J613_25G83_DCP_UUID));version[i]--; } + assert(!j613_25g83_identity(false,1,version,3,J613_25G83_DCP_UUID)); + assert(!j613_25g83_identity(true,0,version,3,J613_25G83_DCP_UUID)); + assert(!j613_25g83_identity(true,1,version,2,J613_25G83_DCP_UUID)); + assert(!j613_25g83_identity(true,1,version,3,"other image")); + assert(j613_25g83_external_state(state,4)); + for(unsigned i=0;i<4;i++)assert(!j613_25g83_external_state(state,i)); + assert(!j613_25g83_external_state(NULL,4)); + for(unsigned bit=0;bit<32;bit++) { + state[1]=16^(1U< +#include +#include +#include +#include +#include +#include +#define __init +#define GFP_KERNEL 0 +#define kzalloc_obj(x) zalloc(sizeof(x)) +#define raw_spin_lock_irqsave(l,f) ((f)=0) +#define raw_spin_unlock_irqrestore(l,f) ((void)(f)) +#define of_prop_cmp strcmp +#define __of_remove_property_sysfs(n,p) ((void)0) +#define __of_update_property_sysfs(n,p,o) ((void)0) +struct property { char *name; void *value; unsigned length; struct property *next; }; +struct device_node { struct property *properties,*deadprops; }; +struct of_changeset {int unused;};static struct of_changeset external_gate; +enum {OF_RECONFIG_ATTACH_NODE,OF_RECONFIG_DETACH_NODE,OF_RECONFIG_ADD_PROPERTY,OF_RECONFIG_REMOVE_PROPERTY,OF_RECONFIG_UPDATE_PROPERTY}; +struct of_changeset_entry {int action;struct device_node *np;struct property *prop,*old_prop;}; +static struct of_changeset_entry queued;static int queue_error,alloc_fail,alloc_calls,allocations; +static void *allocate(size_t n) { if (++alloc_calls==alloc_fail)return NULL;void *p=malloc(n);assert(p);allocations++;return p; } +static void *zalloc(size_t n){void *p=allocate(n);if(p)memset(p,0,n);return p;} +static char *kstrdup(const char *s,int f){(void)f;char *p=allocate(strlen(s)+1);if(p)strcpy(p,s);return p;} +static void *kmemdup(const void *s,size_t n,int f){(void)f;void *p=allocate(n);if(p)memcpy(p,s,n);return p;} +static void kfree(void *p){if(p){allocations--;free(p);}} +static int of_changeset_update_property(struct of_changeset *c,struct device_node *n,struct property *p){(void)c;if(queue_error)return queue_error;queued=(struct of_changeset_entry){OF_RECONFIG_UPDATE_PROPERTY,n,p,NULL};return 0;} +''' +code=pre+'\n'.join([fn(base,'__of_remove_property_from_list'),fn(base,'__of_update_property'),fn(base,'__of_remove_property'),fn(dyn,'__of_changeset_entry_invert'),fn(a,'j613_external_update')])+r''' +static void apply(void){assert(queued.action==OF_RECONFIG_UPDATE_PROPERTY);assert(!__of_update_property(queued.np,queued.prop,&queued.old_prop));} +static void revert(void){struct of_changeset_entry e;__of_changeset_entry_invert(&queued,&e);if(e.action==OF_RECONFIG_REMOVE_PROPERTY)assert(!__of_remove_property(e.np,e.prop));else{assert(e.action==OF_RECONFIG_UPDATE_PROPERTY);assert(!__of_update_property(e.np,e.prop,&e.old_prop));}} +int main(void){ + unsigned char oldbytes[4]={0,0,0,4},newbytes[4]={0,0,0,15};struct property old={.name="apple,min-state",.value=oldbytes,.length=4};struct device_node n={.properties=&old}; + assert(!j613_external_update(&n,"apple,min-state",newbytes,4));assert(n.properties==&old);apply();assert(n.properties->length==4&&!memcmp(n.properties->value,newbytes,4));revert();assert(n.properties==&old&&!memcmp(old.value,oldbytes,4)); + n=(struct device_node){0};assert(!j613_external_update(&n,"apple,min-state",newbytes,4));apply();revert();assert(!n.properties); + n=(struct device_node){0};assert(!j613_external_update(&n,"apple,always-on",NULL,0));apply();assert(n.properties&&!n.properties->length);revert();assert(!n.properties); + struct property boolean={.name="apple,always-on"};n=(struct device_node){.properties=&boolean};assert(!j613_external_update(&n,"apple,always-on",NULL,0));apply();revert();assert(n.properties==&boolean); + old.value=newbytes;n=(struct device_node){.properties=&old};assert(!j613_external_update(&n,"apple,min-state",newbytes,4));apply();revert();assert(n.properties==&old&&!memcmp(n.properties->value,newbytes,4)); + for(int i=1;i<=3;i++){alloc_calls=0;alloc_fail=i;int before=allocations;assert(j613_external_update(&n,"apple,min-state",newbytes,4)==-ENOMEM);assert(allocations==before&&n.properties==&old);} + alloc_fail=0;queue_error=-EIO;int before=allocations;assert(j613_external_update(&n,"apple,min-state",newbytes,4)==-EIO);assert(allocations==before&&n.properties==&old); + puts("PASS actual native UPDATE builder and Linux OF apply/revert: existing/absent/equal u32, existing/absent bool, allocation/queue failures"); +} +''' +with tempfile.TemporaryDirectory() as t: + p=Path(t);(p/'test.c').write_text(code);subprocess.run([os.environ.get('CC','clang'),'-std=gnu11','-Wall','-Wextra','-Werror','-fsanitize=address,undefined',str(p/'test.c'),'-o',str(p/'test')],check=True);subprocess.run([str(p/'test')],check=True,env={**os.environ,'ASAN_OPTIONS':'detect_leaks=0'}) diff --git a/tools/asahi/dcpext26-edid-tests.py b/tools/asahi/dcpext26-edid-tests.py new file mode 100644 index 00000000000000..5244bd35f6fb9e --- /dev/null +++ b/tools/asahi/dcpext26-edid-tests.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Native AV identity/attachment predicates and public EDID wire validation.""" +from pathlib import Path +import os,re,subprocess,tempfile +ROOT=Path(__file__).resolve().parents[2] +source=(ROOT/'drivers/gpu/drm/apple/epic/dpavservep.c').read_text() +def function(name): + m=re.search(r'^(?:static )?bool '+name+r'\(',source,re.M);assert m + return source[m.start():source.index('\n}',m.start())+2] +pre=r''' +#include +#include +#include +#include +#include +#include "epic/dpavserv-edid.h" +typedef int64_t s64; +#define DPAVSERV_ENDPOINT 0x28 +#define DPAVSERV_ENDPOINT_V2 0x2a +#define U16_MAX 65535 +#define DCP_IOMFB_METHODS_H17P_EXTERNAL 2 +#define READ_ONCE(x) (x) +#define lockdep_assert_held(x) ((void)0) +struct apple_epic_service; +struct apple_dcp { + bool native,typec_cable_connected,quiescing,external_physically_absent;u64 typec_generation;int hpd_mutex; + struct { int iomfb_method_profile; } hw; + struct { int lock;bool enabled;u64 epoch;struct apple_epic_service *service; } dcpavserv; +}; +struct ep { struct apple_dcp *dcp;u32 endpoint;bool wire_v2; }; +struct apple_epic_service { struct ep *ep;u32 channel; }; +static bool dcp_is_ext26(struct apple_dcp *d) { return d->native; } +''' +check=r''' +static void checksum(u8 *b) { u8 sum=0;b[127]=0;for(unsigned i=0;i<127;i++)sum+=b[i];b[127]=-sum; } +int main(void) { + struct apple_dcp d={.native=true,.typec_cable_connected=true,.typec_generation=7}; + struct ep ep={&d,0x28,true};struct apple_epic_service svc={&ep,1}; + assert(dcpavserv_identity(&svc,"dcpav-service-epic","DCPDPService",0)); + assert(!dcpavserv_identity(&svc,"dcpav-service-epic","DCPDPService",1)); + assert(!dcpavserv_identity(&svc,"dcpav-service-epic","DCPDP13Service",0)); + ep.endpoint=0x2a;assert(!dcpavserv_identity(&svc,"dcpav-service-epic","DCPDPService",0)); + ep.endpoint=0x28;svc.channel=0;assert(!dcpavserv_identity(&svc,"dcpav-service-epic","DCPDPService",0));svc.channel=1; + d.dcpavserv.enabled=true;d.dcpavserv.service=&svc;d.dcpavserv.epoch=10; + assert(dcpavserv_edid_current(&d,7)&&dcpavserv_edid_service_current(&d,10)); + assert(!dcpavserv_edid_current(&d,6)&&!dcpavserv_edid_service_current(&d,9)); + d.external_physically_absent=true;assert(!dcpavserv_edid_current(&d,7)&&!dcpavserv_edid_service_current(&d,10)); + d.external_physically_absent=false;d.quiescing=true;assert(!dcpavserv_edid_current(&d,7)); + d.quiescing=false;d.typec_cable_connected=false;assert(!dcpavserv_edid_current(&d,7)); + u8 wire[48+8+256]={0};u8 *base=wire+56;const u8 header[]={0,255,255,255,255,255,255,0}; + afkv2_put64(wire,EDID_BUF_SIZE);afkv2_put64(wire+32,8+256);memcpy(base,header,8);base[126]=1;checksum(base);checksum(base+128); + size_t size=0;assert(!dpavserv_edid_size(wire,sizeof(wire),&size)&&size==256); + for(size_t n=0;n +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; typedef uint64_t u64; typedef uint8_t u8; +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,y) ((x)=(y)) +#define lockdep_assert_held(x) ((void)0) +#define ARRAY_SIZE(x) (sizeof(x)/sizeof(*(x))) +#define IS_ERR_OR_NULL(x) (!(x)) +#define scoped_guard(...) for (bool once=true;once;once=false) +#define dev_info(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define dev_warn(...) ((void)0) +#define dev_err(...) mock_log(__VA_ARGS__) +static void mock_log(int dev,const char *fmt,...) { (void)dev;(void)fmt; } +#define msecs_to_jiffies(x) (x) +#define DCP_SWAP_WATCHDOG_MS 2000 +#define DCP_EXTERNAL_STOP_RETRIES 3 +#define AFK_MAX_CHANNEL 3 +#define MAX_PENDING_CMDS 16 +#define spin_lock_irqsave(l,f) ((void)(l),(void)(&(f))) +#define spin_unlock_irqrestore(l,f) ((void)(l),(void)(&(f))) +static bool bitmap_empty(const unsigned long *v,unsigned n) { (void)n;return !*v; } +struct apple_epic_service { int lock; unsigned long cmd_map[1]; }; +struct ring { bool ready; u32 *rptr,*wptr; }; +struct apple_dcp_afkep { int lock,wq; struct ring rxbfr,txbfr; struct apple_epic_service services[AFK_MAX_CHANNEL]; }; +struct port { bool connected; int *service; }; +struct ext26_control; +#define SWAP_SURFACES 4 +#define guard(x) (void) +struct drm_gem_object { int tag; }; +struct drm_framebuffer { struct drm_gem_object *obj[1]; }; +struct iomfb_scanout_h17p { struct drm_framebuffer *fb[4]; struct { struct iomfb_scanout_h17p *next; } link; }; +#define list_for_each_entry(pos,head,member) for ((pos)=(head)->first;(pos);(pos)=(pos)->member.next) +struct apple_dcp { + struct { int lock; } iomfb; struct { struct iomfb_scanout_h17p *first; } ext26_retained; + bool external_suspended,ext26_pm_preparing,ext26_pm_work_off;u8 *coproc_reg; + u64 external_generation; int dev,external_error; bool external_stopped,external_power_attempted,external_running; + bool external_stop_deferred,pipe_enabled_h17p,typec_crtc_off,ext26_normal,ext26_stopping; + unsigned external_stop_retries; int external_lock,hpd_mutex,external_stop_retry_wq,rtk,avep; + struct port dptxport[1]; + struct apple_dcp_afkep *ext26_aux[3]; + struct apple_dcp_afkep *systemep,*ibootep,*dptx_controller_ep,*dptxep,*dcpavservep; + struct ext26_control *ext26_control; u32 ext26_control_id; bool ext26_control_pending,crashed,ext26_blocked; +}; +#define APPLE_DCP_COPROC_CPU_CONTROL 0x44 +#define APPLE_DCP_COPROC_CPU_CONTROL_RUN 16 +static u32 readl(const void *p) { u32 v;memcpy(&v,p,4);return v; } +static unsigned discoveries; +static void dcp_enable_typec_work(struct apple_dcp *d) { (void)d;discoveries++; } +static bool dcp_is_ext26(struct apple_dcp *d) { (void)d;return true; } +static char trace[128]; static unsigned used; static char fault; static unsigned settlements; +static void note(char c) { assert(usedservice; } +static int dptxport_set_hpd(int *s,bool on) { assert(s&&!on);return result('H'); } +static int dptxport_release_display(int *s) { assert(s);return result('R'); } +static void apple_rtkit_flush_rx(int r) { (void)r;note('X'); } +static void flush_workqueue(int q) { (void)q;note('W'); } +static int iomfb_ext26_retain_epoch(struct apple_dcp *d) { (void)d;return result('K'); } +static void dpavservep_detach(struct apple_dcp *d) { (void)d;note('E'); } +static void dcp_modes_begin_attachment(struct apple_dcp *d) { (void)d;note('M'); } +static void av_service_disconnect(struct apple_dcp *d) { (void)d;note('A'); } +static int system_dfl_wq; +static void mod_delayed_work(int q,int *w,int t) { (void)q;(void)w;(void)t;note('D'); } +struct completion { unsigned signaled; }; +static void complete_all(struct completion *c) { c->signaled++; } +#define DCPEXT26_SUBMIT_BYTES 0x1bd8 +struct ext26_control { struct dcpext26_exchange exchange; struct completion done; u8 wire[DCPEXT26_SUBMIT_BYTES]; }; +''' +checks = r''' +static struct apple_dcp fresh(struct apple_dcp_afkep *ep) { + static int service; struct apple_dcp d={0}; d.external_generation=7; d.external_power_attempted=true; + d.external_running=true; d.pipe_enabled_h17p=true; d.ext26_normal=true; + d.dptxport[0].connected=true; d.dptxport[0].service=&service; d.systemep=ep; + used=settlements=0;trace[0]=0;fault=0;return d; +} +static void native_pm_complete(struct apple_dcp *dcp); +static int native_pm_resume(struct apple_dcp *dcp); +int main(void) { + struct ext26_control c={0}; struct apple_dcp d={0};d.ext26_control=&c;d.ext26_control_pending=true; + assert(!dcpext26_exchange_begin(&c.exchange,0)); + assert(dcpext26_exchange_begin(&c.exchange,42));assert(!dcpext26_exchange_begin(&c.exchange,43)); + assert(!iomfb_ext26_control_complete(&d,41,false));assert(!c.done.signaled); + assert(iomfb_ext26_control_complete(&d,42,false));assert(!c.done.signaled&&d.ext26_control_pending); + c.exchange.replied=true; ext26_control_signal(&d,&c);assert(c.done.signaled&&!d.ext26_control_pending); + c=(struct ext26_control){0};d.ext26_control=&c;d.ext26_control_pending=true; + assert(dcpext26_exchange_begin(&c.exchange,51));c.exchange.replied=true; + ext26_control_signal(&d,&c);assert(!c.done.signaled); + assert(iomfb_ext26_control_complete(&d,51,true));assert(c.exchange.failed&&c.done.signaled); + assert(!dcpext26_epoch_fits(8,1,1)); assert(dcpext26_epoch_fits(7,0,(960ULL<<20))); + assert(!dcpext26_epoch_fits(7,(960ULL<<20),1));assert(!dcpext26_epoch_fits(0,~0ULL,1)); + struct apple_dcp_afkep ep={0};u32 read=0,write=0; + ep.rxbfr=(struct ring){true,&read,&write};assert(afk_host_idle(&ep)); + write=64;assert(!afk_host_idle(&ep));read=64;assert(afk_host_idle(&ep)); + ep.services[1].cmd_map[0]=1;assert(!afk_host_idle(&ep));ep.services[1].cmd_map[0]=0; + d=fresh(&ep);assert(!dcp_external_stop_locked(&d,6)&&!used); /* stale stop token */ + assert(!dcp_external_stop_locked(&d,7));assert(!strcmp(trace,"FEQPQHRXWXQKM")); + assert(d.external_stopped&&!d.pipe_enabled_h17p&&!d.ext26_normal&&!d.ext26_stopping); + assert(d.typec_crtc_off&&!d.dptxport[0].connected); + assert(!dcp_external_stop_locked(&d,7)); /* idempotent stop */ + d=fresh(&ep);fault='P';assert(dcp_external_stop_locked(&d,7)==-EIO); + assert(d.external_error==-EIO&&!d.external_stopped&&d.dptxport[0].connected); + assert(!strcmp(trace,"FEQP"));assert(dcp_external_stop_locked(&d,7)==-EIO); + d=fresh(&ep);fault='H';assert(dcp_external_stop_locked(&d,7)==-EIO);assert(!strchr(trace,'R')); + d=fresh(&ep);fault='R';assert(dcp_external_stop_locked(&d,7)==-EIO);assert(!strchr(trace,'K')); + d=fresh(&ep);ep.services[0].cmd_map[0]=1;assert(dcp_external_stop_locked(&d,7)==-EBUSY); + assert(!strchr(trace,'K'));ep.services[0].cmd_map[0]=0; + d=fresh(&ep);d.external_power_attempted=false;assert(!dcp_external_stop_locked(&d,7)); + assert(!strchr(trace,'P')&&strchr(trace,'K')); /* no-sink / pre-picture stop */ + d=fresh(&ep);assert(d.external_running);assert(!dcp_external_start_locked(&d));assert(!strcmp(trace,"LU")); + d=fresh(&ep);d.external_running=false;assert(!dcp_external_start_locked(&d));assert(d.external_running&&!strcmp(trace,"LU")); + struct drm_gem_object oldobj={1},newobj={2}; + struct drm_framebuffer oldfb={{&oldobj}},aliasfb={{&oldobj}},newfb={{&newobj}}; + struct iomfb_scanout_h17p retained={.fb={&oldfb}};d.ext26_retained.first=&retained; + assert(!iomfb_ext26_backing_fresh(&d,&oldfb));assert(!iomfb_ext26_backing_fresh(&d,&aliasfb)); + assert(iomfb_ext26_backing_fresh(&d,&newfb)); + u8 registers[0x50]={0};u32 running=16;memcpy(registers+0x44,&running,4); + d=(struct apple_dcp){.coproc_reg=registers,.ext26_pm_preparing=true,.ext26_pm_work_off=true,.external_suspended=true}; + assert(!native_pm_resume(&d));assert(!discoveries&&d.ext26_pm_preparing&&d.ext26_pm_work_off); + native_pm_complete(&d);assert(discoveries==1&&!d.ext26_pm_preparing&&!d.ext26_pm_work_off); + native_pm_complete(&d);assert(discoveries==1); + memset(registers,0,sizeof(registers));d.ext26_pm_preparing=d.ext26_pm_work_off=true; + assert(native_pm_resume(&d)==-EHOSTDOWN&&d.ext26_blocked&&d.external_error==-EHOSTDOWN); + native_pm_complete(&d);assert(discoveries==1); /* a dead coprocessor gets no discovery */ + puts("PASS native26 exact control order, stale IDs, abort, retained-stop sequencing/faults, host drain, old-GEM rejection, epoch bounds and PM-complete ordering"); +} +''' +life = (ROOT/'drivers/gpu/drm/apple/dcpext26-lifecycle.h').read_text().replace('#include ','') +# Put the pure state before the mock control container. +pre = pre.replace('struct ext26_control { struct dcpext26_exchange',life+'\nstruct ext26_control { struct dcpext26_exchange') +code = pre + '\n' + '\n'.join([ + body('drivers/gpu/drm/apple/afk.c','afk_host_idle'), + body('drivers/gpu/drm/apple/iomfb.c','iomfb_ext26_backing_fresh'), + body('drivers/gpu/drm/apple/iomfb_h17p.c','ext26_control_signal'), + body('drivers/gpu/drm/apple/iomfb_h17p.c','iomfb_ext26_control_complete'), + body('drivers/gpu/drm/apple/dcp.c','dcp_external_stop_locked')]) + checks +# dcp.c has a forward declaration; select the actual definition. +s=(ROOT/'drivers/gpu/drm/apple/dcp.c').read_text();pos=s.index('static int dcp_external_stop_locked(struct apple_dcp *dcp, u64 generation)\n{');actual=s[pos:s.index('\n}',pos)+2] +code=code[:code.index('static int dcp_external_stop_locked')] + actual + '\n' + body('drivers/gpu/drm/apple/dcp.c','dcp_external_start_locked') + checks +# Execute the native branches of the actual PM hooks, preserving their returns. +def native_branch(name): + pos=s.index('static '+('void' if name.endswith('complete') else 'int')+' '+name+'(') + pos=s.index('if (dcp_is_ext26(dcp)) {',pos) + opening=s.index('{',pos);end=opening+1;depth=1 + while depth: + depth+=(s[end]=='{')-(s[end]=='}');end+=1 + return s[pos:end] +code += '\nstatic void native_pm_complete(struct apple_dcp *dcp) { '+native_branch('dcp_platform_complete')+' }\n' +code += 'static int native_pm_resume(struct apple_dcp *dcp) { int dev=0; '+native_branch('dcp_platform_resume')+' return -EINVAL; }\n' + +with tempfile.TemporaryDirectory() as t: + p=Path(t);(p/'check.c').write_text(code) + subprocess.run([os.environ.get('CC','clang'),'-std=gnu11','-Wall','-Wextra','-Werror','-fsanitize=address,undefined',str(p/'check.c'),'-o',str(p/'check')],check=True) + subprocess.run([str(p/'check')],check=True) diff --git a/tools/asahi/dcpext26-modeset-tests.py b/tools/asahi/dcpext26-modeset-tests.py new file mode 100644 index 00000000000000..0c3afa1b0638dc --- /dev/null +++ b/tools/asahi/dcpext26-modeset-tests.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Exercise native preflight with DRM's detached CRTC state after state swap.""" +from pathlib import Path +import subprocess,tempfile +R=Path(__file__).resolve().parents[2];s=(R/'drivers/gpu/drm/apple/iomfb_template.c').read_text();caller=(R/'drivers/gpu/drm/apple/iomfb.c').read_text() +a=s.index('\tif (dcp_is_ext26(dcp)) {',s.index('int DCP_FW_NAME(iomfb_modeset)')) +b=s.index('\n\tif (!lookup_mode',a) +a=s.rfind('#if DCP_FW_VER',0,a) +body=s[a:b] +assert 'crtc_state->state' not in body +assert 'drm_atomic_get_new_plane_state(commit, crtc->primary)' in caller +pre=r''' +#include +#include +#include +#include +typedef uint32_t u32; +#define DCP_FW_VERSION(a,b,c) ((a)*10000+(b)*100+(c)) +#define DCP_FW_VER DCP_FW_VERSION(26,6,0) +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) +struct mode {int id;}; +struct drm_framebuffer {u32 pitches[1];}; +struct drm_plane_state {struct drm_framebuffer *fb;}; +struct drm_crtc_state {void *state;struct mode mode;}; +struct apple_dcp {bool ext26_blocked,pipe_enabled_h17p,ext26_blanked,ext26_normal,ext26_power_sent;u32 ext26_stride,ext26_generation,typec_generation;struct mode ext26_mode;int ext26_ready;}; +static bool dcp_is_ext26(struct apple_dcp *d){return true;} +static bool drm_mode_equal(const struct mode *a,const struct mode *b){return a->id==b->id;} +static unsigned stops; +static int dcp_ext26_layout_stop(struct apple_dcp *d){stops++;d->pipe_enabled_h17p=false;return 0;} +#define reinit_completion(c) (*(c)=0) +''' +code=pre+'static int prepare(struct apple_dcp *dcp,struct drm_crtc_state *crtc_state,struct drm_plane_state *primary){int ret;\n'+body+'\nreturn 0;}'+r''' +int main(void){ + struct drm_crtc_state c={.state=0,.mode.id=1};struct drm_framebuffer fb={.pitches={15360}};struct drm_plane_state p={.fb=&fb};struct apple_dcp d={.typec_generation=7}; + assert(!prepare(&d,&c,&p)&&d.ext26_stride==15360&&d.ext26_generation==7); + d.pipe_enabled_h17p=true;d.ext26_blanked=true;d.ext26_mode=c.mode; + assert(!prepare(&d,&c,&p)&&!d.ext26_blanked&&!stops); + // G's post-resume repeat must preserve the already proven active mode. + d.ext26_normal=true;d.ext26_power_sent=true;d.ext26_ready=1; + assert(!prepare(&d,&c,&p)&&d.ext26_normal&&d.ext26_power_sent&&d.ext26_ready==1&&!stops); + // A different physical generation must still acquire fresh readiness. + d.typec_generation++;assert(!prepare(&d,&c,&p)&&!d.ext26_normal&&!d.ext26_power_sent&&d.ext26_ready==0); + fb.pitches[0]=16384;assert(!prepare(&d,&c,&p)&&stops==1&&d.ext26_stride==16384); + d.ext26_blocked=true;assert(prepare(&d,&c,&p)==-EIO); + d.ext26_blocked=false;assert(!prepare(&d,&c,0)); +} +''' +with tempfile.TemporaryDirectory() as t: + p=Path(t)/'test.c';p.write_text(code);subprocess.run(['clang','-std=gnu11','-Wall','-Werror','-fsanitize=address,undefined',str(p),'-o',str(p.with_suffix(''))],check=True);subprocess.run([str(p.with_suffix(''))],check=True) +print('PASS actual native preflight: detached CRTC atomic backlink, 4K pitch, gentle resume, changed layout, missing plane and blocked session') + +# The driver enable callback must finish route ownership and wake a pending flip +# on native modeset errors; the DRM core, not this callback, owns commit completion. +drv=(R/'drivers/gpu/drm/apple/apple_drv.c').read_text() +a=drv.index('static void apple_crtc_atomic_enable(');b=drv.index('\n}',a)+2 +actual=drv[a:b] +preamble=r''' +#include +#include +#include +#define dev_warn(...) ((void)0) +#define dev_err(...) ((void)0) +struct drm_crtc {int unused;}; +struct drm_crtc_state {bool active,active_changed,color_mgmt_changed;}; +struct drm_atomic_commit {struct drm_crtc_state *state;unsigned failed_routes;}; +struct apple_dcp {void *dev;bool native,main_display;int mode_state,vblank_wq;void *connector;}; +struct apple_crtc {struct apple_dcp *dcp;bool fabric_held;}; +static struct apple_crtc owner; +static struct apple_crtc *to_apple_crtc(struct drm_crtc *c){return &owner;} +static struct apple_dcp *platform_get_drvdata(struct apple_dcp *d){return d;} +static struct drm_crtc_state *drm_atomic_get_new_crtc_state(struct drm_atomic_commit *c,struct drm_crtc *r){return c->state;} +#define to_apple_atomic_state(c) (c) +static unsigned drm_crtc_mask(struct drm_crtc *c){return 1;} +static bool dcp_is_ext26(struct apple_dcp *d){return d->native;} +static unsigned done,woken,notified,invalidated,powered,programmed; +static int mode_error; +static int dcp_typec_follow_crtc(struct drm_crtc *c,struct drm_atomic_commit *a,bool *locked){*locked=true;return 0;} +static void dcp_typec_follow_done(bool locked){assert(locked);done++;} +static bool dcp_crtc_route_ready(struct drm_crtc *c,struct drm_atomic_commit *a,bool enable){return true;} +static bool dcp_crtc_needs_route_start(struct apple_dcp *d){return false;} +static int dcp_typec_prepare_route(struct apple_dcp *d){return 0;} +static int apple_drm_fabric_hold(void){return 0;} +static void dcp_poweron(struct apple_dcp *d){powered++;} +static void dcp_external_recover(struct apple_dcp *d){} +static bool drm_atomic_crtc_needs_modeset(struct drm_crtc_state *s){return true;} +static int dcp_crtc_atomic_modeset(struct drm_crtc *c,struct drm_atomic_commit *a){programmed++;return mode_error;} +static void dcp_mode_invalidate(int *m){invalidated++;*m=0;} +static void schedule_work(int *w){woken++;} +static void dcp_route_failure_notify(void *c){notified++;} +''' +checks=r''' +int main(void){ + struct drm_crtc crtc={0};struct drm_crtc_state state={.active=true,.active_changed=true}; + struct drm_atomic_commit commit={.state=&state};struct apple_dcp d={.native=true,.connector=(void *)1};owner.dcp=&d; + const int errors[]={-EIO,-ENOMEM,-ETIMEDOUT,-EPIPE}; + for(unsigned i=0;i<4;i++){ + done=woken=notified=invalidated=powered=programmed=0;mode_error=errors[i];d.mode_state=1; + apple_crtc_atomic_enable(&crtc,&commit); + assert(done==1&&woken==1&¬ified==1&&invalidated==1&&programmed==1&&powered==1&&!d.mode_state); + } + done=woken=notified=invalidated=0;mode_error=0; + apple_crtc_atomic_enable(&crtc,&commit);assert(done==1&&!woken&&!notified&&!invalidated); + // Existing non-native error policy is preserved. + d.native=false;done=woken=notified=invalidated=0;mode_error=-EIO; + apple_crtc_atomic_enable(&crtc,&commit);assert(done==1&&!woken&&!notified&&!invalidated); +} +''' +with tempfile.TemporaryDirectory() as t: + p=Path(t)/'enable.c';p.write_text(preamble+actual+checks) + subprocess.run(['clang','-std=gnu11','-Wall','-Werror','-fsanitize=address,undefined',str(p),'-o',str(p.with_suffix(''))],check=True) + subprocess.run([str(p.with_suffix(''))],check=True) +print('PASS actual enable callback: native mode errors invalidate/wake flips/finish route once; success and classic policy preserved') diff --git a/tools/asahi/dcpext26-parity-tests.py b/tools/asahi/dcpext26-parity-tests.py new file mode 100644 index 00000000000000..4cb08bba288edf --- /dev/null +++ b/tools/asahi/dcpext26-parity-tests.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Ordered native startup and the native DP callback contract, actual C bodies. + +Synthetic transport/MMIO boundaries. This cannot prove a physical picture or PM. +""" +from pathlib import Path +import os,re,subprocess,tempfile +R=Path(__file__).resolve().parents[2] +def fn(s,n): + m=re.search(r'^(?:static )?(?:int|bool|void) '+n+r'\([^;]*?\)\n\{',s,re.M|re.S);assert m,n + i=m.end();depth=1 + while depth:depth+=(s[i]=='{')-(s[i]=='}');i+=1 + return s[m.start():i] +def run(name,code): + with tempfile.TemporaryDirectory() as t: + p=Path(t)/'test.c';p.write_text(code) + subprocess.run([os.environ.get('CC','clang'),'-std=gnu11','-Wall','-Wextra','-Werror','-Wno-unused-parameter','-Wno-missing-field-initializers','-Wno-sign-compare','-fsanitize=address,undefined',str(p),'-o',str(p.with_suffix(''))],check=True) + subprocess.run([str(p.with_suffix(''))],check=True) + print('PASS',name) +dcp=(R/'drivers/gpu/drm/apple/dcp.c').read_text() +pre=r''' +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8;typedef uint16_t u16;typedef uint32_t u32;typedef uint64_t u64; +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,y) ((x)=(y)) +#define ARRAY_SIZE(x) (sizeof(x)/sizeof(*(x))) +#define IS_ERR(p) ((intptr_t)(p)<0) +#define PTR_ERR(p) ((int)(intptr_t)(p)) +#define ERR_PTR(e) ((void *)(intptr_t)(e)) +#define dev_info(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define APPLE_DCP_COPROC_CPU_CONTROL 0x44 +#define APPLE_DCP_COPROC_CPU_CONTROL_RUN 16 +#define APPLE_DCP_COPROC_CPU_STATUS 0x48 +#define msecs_to_jiffies(x) (x) +struct device {int unused;}; +struct apple_dcp_afkep {u8 endpoint;bool match_epic_name;}; +struct apple_dcp {struct device *dev;u8 *coproc_reg;void *rtk;bool crashed;int ext26_link_ready; + struct apple_dcp_afkep *ext26_aux[3];}; +static char trace[256];static unsigned used;static char fault; +static void note(char c){assert(used<255);trace[used++]=c;trace[used]=0;} +static int step(char c){note(c);return fault==c?-EIO:0;} +static void reset(char f){memset(trace,0,sizeof(trace));used=0;fault=f;} +static int dev_err_probe(struct device *d,int e,const char *fmt,...){return e;} +static u32 readl(void *p){u32 x;memcpy(&x,p,4);return x;} +static void writel(u32 x,void *p){assert(x&16);note('R');memcpy(p,&x,4);} +static int dcp_pin_live_session(struct apple_dcp *d){return step('P');} +static const int rtkit_ops; +static void *apple_rtkit_init(struct device *dev,struct apple_dcp *d,const char *name,int channel,const int *ops){assert(!strcmp(name,"mbox")&&!channel&&ops==&rtkit_ops);return step('I')?ERR_PTR(-EIO):d;} +static void dcp_release_rtkit(void *d){} +static int devm_add_action_or_reset(struct device *d,void (*action)(void *),void *p){return step('A');} +static int apple_rtkit_wake(void *r){return step('W');} +static int apple_rtkit_boot(void *r){return step('B');} +static int iomfb_start_rtkit(struct apple_dcp *d){return step('L');} +static unsigned wait_for_completion_timeout(int *c,unsigned t){note('l');return fault!='l';} +static int systemep_init(struct apple_dcp *d){return step('0');} +static int ibootep_init(struct apple_dcp *d){return step('3');} +static int dptx_controller_init(struct apple_dcp *d){return step('4');} +static int dpavservep_init(struct apple_dcp *d){return step('8');} +static int dptxep_init(struct apple_dcp *d){return step('a');} +static const int ext26_aux_ops[1]; +static struct apple_dcp_afkep aux[3];static unsigned auxiliary; +static struct apple_dcp_afkep *afk_init(struct apple_dcp *d,u8 ep,const int *ops){assert(ops==ext26_aux_ops);struct apple_dcp_afkep *a=&aux[auxiliary++%3];a->endpoint=ep;return a;} +static int afk_start(struct apple_dcp_afkep *ep){char c=ep->endpoint==0x27?'7':ep->endpoint==0x29?'9':'c';return step(c);} +static void iomfb_ext26_start(struct apple_dcp *d){note('S');} +''' +checks=r''' +int main(void){ + u8 mmio[0x50]={0};struct device dev={0};struct apple_dcp d={.dev=&dev,.coproc_reg=mmio};u32 run=16; + reset(0);memcpy(mmio+0x44,&run,4);assert(!dcp_ext26_start_resident(&d));assert(!strcmp(trace,"PIAW")); + reset(0);memset(mmio,0,sizeof(mmio));assert(!dcp_ext26_start_resident(&d));assert(!strcmp(trace,"PIARB")&&readl(mmio+0x44)==16); + const char stages[]="PIAWB"; + for(unsigned i=0;idp.lanes=capacity;return fault;} +static int dcp_direct_crossbar_link(struct apple_dcp *d,bool on){if(on)up++;else down++;return fault;} +static void mdelay(unsigned n){assert(n==10);delay++;} +static void complete(int *c){(*c)++;} +static int dptx_j700_training(struct apple_epic_service *s,bool w,const void *d,size_t n,void *r,size_t room){training++;return 0;} +static int dptx_j700_validate_lanes(struct dptx_port *p,u64 n){return n<=capacity&&n!=3?0:-EINVAL;} +static int dptxport_call_set_link_rate(struct apple_epic_service *s,const void *d,size_t n,void *r,size_t room){assert(down);return 0;} +static int dptxport_call(struct apple_epic_service *s,u32 cmd,const void *d,size_t n,void *r,size_t room){fallback++;return 0;} +''' +run('native DP callback wire and ordering',pre2+fn(s,'dptxport_native26_call')+r''' +int main(void){ + struct apple_dcp d={0};struct ep ep={&d};struct dptx_port p={.atcphy=&d};struct apple_epic_service svc={&ep,&p};u8 input[160]={0},out[160]; + #define CALL(c,n,r) dptxport_native26_call(&svc,0,(c),input,(n),out,(r)) + memset(out,0xa5,sizeof(out));assert(!CALL(2,32,32));assert(word(out+20)==3&&word(out+24)==3&&!word(out+16)); + assert(!CALL(7,32,32)&&word(out+16)==0x14); + assert(!CALL(18,32,32)&&word(out+16)==1);assert(!CALL(21,160,160)&&word(out)==1); + assert(!CALL(10,32,32)&&p.native26_max_lanes==4&&!p.lane_count); + put_unaligned_le64(4,input+16);assert(!CALL(12,32,32)&&p.lane_count==4&&p.linkcfg_completion==1); + put_unaligned_le64(3,input+16);assert(CALL(12,32,32)==-EINVAL); + assert(!CALL(0,16,16)&&up==1);assert(!CALL(1,16,16)&&down==1); + assert(!CALL(5,16,16)&&down==2);p.link_rate=0x14;assert(!CALL(6,16,16)&&delay==1&&up==2); + fault=-EIO;assert(CALL(0,16,16)==-EIO);put_unaligned_le64(0x14,input+16);assert(CALL(9,32,32)==-EIO&&!fallback);fault=0; + put_unaligned_le64(0x99,input+16);assert(CALL(9,32,32)==-EINVAL); + assert(!dptxport_native26_call(&svc,1,3,input,80,out,80)&&training==1); + assert(dptxport_native26_call(&svc,2,3,input,80,out,80)==-EOPNOTSUPP); + puts("native DP HPD/tiled=1, drive offsets20/24, maxHBR2, distinct max/active lanes, crossbar errors propagated"); +} +''') +# Deferred OPEN executes only after A401, including ops without a prepare/revalidate hook. +s=(R/'drivers/gpu/drm/apple/afk.c').read_text() +pre3=pre[:pre.index('struct device {')]+r''' +#define kfree(p) ((void)0) +#define dev_err(...) ((void)0) +#define EPIC_TYPE_NOTIFY 1 +#define EPIC_CAT_REPORT 2 +#define AFKV2_OPEN 3 +struct apple_epic_service; +struct apple_epic_service_ops {int (*revalidate)(struct apple_epic_service *,const char *,const char *,int);void (*init)(struct apple_epic_service *,const char *,const char *,int);}; +struct apple_epic_service {bool pending_open,enabled;const struct apple_epic_service_ops *ops;const char *pending_name,*pending_class;int pending_unit;u32 channel;}; +struct apple_dcp {bool ext26_services_open;struct apple_dcp_afkep *dptxep;}; +static bool dptx_native_controllers_ready(struct apple_dcp *d){return true;} +struct apple_dcp_afkep {struct apple_dcp *dcp;unsigned num_channels;struct apple_epic_service services[2];}; +static bool dcp_is_ext26(struct apple_dcp *d){return true;} +static unsigned sent,published,discarded,initialized;static int fault; +static void afk_discard_pending_open(struct apple_epic_service *s){discarded++;s->pending_open=false;} +static void *afk_epic_find_service(struct apple_dcp_afkep *e,u32 c){return NULL;} +static int afk_send_epic(struct apple_dcp_afkep *e,u32 c,u16 tag,int type,int cat,u8 subtype,const void *data,size_t size){assert(c&&subtype==AFKV2_OPEN&&!size);sent++;return fault;} +static void afk_service_publish(struct apple_epic_service *s){published++;s->enabled=true;} +static void afk_populate_service_debugfs(struct apple_epic_service *s){} +static void initialized_service(struct apple_epic_service *s,const char *n,const char *c,int u){initialized++;} +''' +run('A401 service OPEN barrier',pre3+fn(s,'afk_retry_service_init')+r''' +int main(void){struct apple_dcp d={0};struct apple_epic_service_ops ops={.init=initialized_service};struct apple_dcp_afkep e={.dcp=&d,.num_channels=1};e.services[0]=(struct apple_epic_service){.pending_open=true,.ops=&ops,.channel=1}; + afk_retry_service_init(&e);assert(!sent&&!published&&!initialized&&e.services[0].pending_open); + d.ext26_services_open=true;afk_retry_service_init(&e);assert(sent==1&&published==1&&initialized==1&&!e.services[0].pending_open&&e.services[0].enabled); + afk_retry_service_init(&e);assert(sent==1);ops.init=NULL;e.services[0].pending_open=true;fault=-EIO;afk_retry_service_init(&e);assert(discarded==1&&published==1); + puts("native deferred OPEN survives absent revalidate/init hooks, opens after A401 once, rejects failed transport");} +''') +# Native property transfer's sentinel count and replacing a prior transfer. +s=(R/'drivers/gpu/drm/apple/iomfb_h17p.c').read_text() +# fn() also accepts the u8 return type for this small callback. +m=re.search(r'^static u8 ext26_prop_start\([^;]*?\)\n\{',s,re.M|re.S);a=m.start();i=m.end();depth=1 +while depth:depth+=(s[i]=='{')-(s[i]=='}');i+=1 +run('native property transfer count',pre[:pre.index('struct device {')]+r''' +#define SZ_1M (1U<<20) +#define GFP_KERNEL 0 +#define kfree free +#define max_t(t,a,b) ((t)(a)>(t)(b)?(t)(a):(t)(b)) +static bool fail_alloc; +static void *kzalloc(size_t n,int f){return fail_alloc?NULL:calloc(1,n);} +struct dcp_chunks {void *data;size_t length,received;bool contiguous;u64 modes_generation;}; +struct apple_dcp {struct dcp_chunks chunks;}; +static u64 generation; +static u64 dcp_modes_transfer_begin(struct apple_dcp *d){return ++generation;} +static void dcp_firmware_metadata_begin(struct apple_dcp *d){} +'''+s[a:i]+r''' +int main(void){struct apple_dcp d={0};assert(!ext26_prop_start(&d,0));assert(!ext26_prop_start(&d,(1U<<20)+2)); + assert(ext26_prop_start(&d,4097)&&d.chunks.length==4096&&d.chunks.contiguous&&!d.chunks.received); + memset(d.chunks.data,0x55,4096);d.chunks.received=11;assert(ext26_prop_start(&d,2)&&d.chunks.length==1&&!d.chunks.received&&!*(u8 *)d.chunks.data); + assert(ext26_prop_start(&d,1)&&d.chunks.length==0&&d.chunks.data);free(d.chunks.data);d.chunks.data=NULL; + fail_alloc=true;assert(!ext26_prop_start(&d,4097)&&!d.chunks.data);puts("native D127 count-1, replaced transfer, empty payload, bounds and allocation refusal");} +''') +# Physical-address allocation and PIODMA lifetime: exercise the common allocator's +# native branch, plus the native wire admission, rather than a model allocator. +s=(R/'drivers/gpu/drm/apple/iomfb_template.c').read_text();pos=s.index('dcpep_cb_allocate_buffer(struct');a=s.rfind('static struct ',0,pos);i=s.index('\n}',pos)+2 +alloc=s[a:i] +h=(R/'drivers/gpu/drm/apple/iomfb_h17p.c').read_text() +pre4=pre[:pre.index('struct device {')]+r''' +#define SZ_16K 16384UL +#define DCP_MAX_MAPPINGS 128 +#define GFP_KERNEL 0 +#define DMA_ATTR_FORCE_CONTIGUOUS 8 +#define DCP_FW_VERSION(a,b,c) ((a)*10000+(b)*100+(c)) +#define DCP_FW_VER DCP_FW_VERSION(26,6,0) +#define DCP_FW_NAME(n) n +#define round_down(x,a) ((x)&~((__typeof__(x))(a)-1)) +#define check_add_overflow(a,b,p) __builtin_add_overflow((a),(b),(p)) +#define dev_warn(...) ((void)0) +struct scatterlist {u64 phys;}; +struct sg_table {struct scatterlist *sgl;unsigned orig_nents;}; +struct dcp_mem_descriptor {size_t size;void *buf;u64 dva;struct sg_table map;bool piodma_mapped;}; +struct apple_dcp {bool native;void *dev;struct dcp_mem_descriptor memdesc[128];unsigned long memdesc_map[2],ext26_released[2];}; +struct dcp_allocate_buffer_req {u64 size;}; +struct dcp_allocate_buffer_resp {u32 mem_desc_id;u64 dva_size,dva,paddr;}; +static bool dcp_is_ext26(struct apple_dcp *d){return d->native;} +static bool test_bit(unsigned id,const unsigned long *b){return !!(b[id/64]&(1UL<<(id%64)));} +static void set_bit(unsigned id,unsigned long *b){b[id/64]|=1UL<<(id%64);} +static unsigned find_first_zero_bit(unsigned long *b,unsigned n){for(unsigned i=0;isgl=&sg;t->orig_nents=fragmented?2:1;return 0;} +static void sg_free_table(struct sg_table *t){} +static u64 sg_phys(struct scatterlist *s){return s->phys;} +static void dma_free_attrs(void *d,size_t n,void *p,u64 a,unsigned attrs){assert(attrs==8);frees++;free(p);} +static void dma_free_coherent(void *d,size_t n,void *p,u64 a){frees++;free(p);} +static u64 get_unaligned_le64(const void *p){u64 v;memcpy(&v,p,8);return v;} +static u32 get_unaligned_le32(const void *p){u32 v;memcpy(&v,p,4);return v;} +static void put_unaligned_le32(u32 v,void *p){memcpy(p,&v,4);} +static void put_unaligned_le64(u64 v,void *p){memcpy(p,&v,8);} +static bool trampoline_map_piodma_h17p(struct apple_dcp *d,int tag,void *out,void *in){u64 id=get_unaligned_le64(in);maps++;d->memdesc[id].piodma_mapped=true;put_unaligned_le32(id,out);put_unaligned_le64(d->memdesc[id].dva,(u8 *)out+4);return true;} +''' +run('native contiguous buffer and PIODMA wire ownership',pre4+alloc+fn(h,'ext26_map_piodma')+r''' +int main(void){struct apple_dcp d={.native=true};set_bit(0,d.memdesc_map);struct dcp_allocate_buffer_req q={4096}; + struct dcp_allocate_buffer_resp r=dcpep_cb_allocate_buffer(&d,&q);assert(r.mem_desc_id==1&&r.dva_size==16384&&r.paddr==0x200000&&r.dva==0x800000&&attributes==8); + u8 in[12]={0},out[16];put_unaligned_le64(1,in);put_unaligned_le32(0,in+8);ext26_map_piodma(&d,201,out,in);assert(!maps&&!get_unaligned_le32(out)); + put_unaligned_le32(1,in+8);ext26_map_piodma(&d,201,out,in);assert(maps==1&&get_unaligned_le32(out)==1&&get_unaligned_le64(out+4)==r.dva&&!get_unaligned_le32(out+12)); + ext26_map_piodma(&d,201,out,in);assert(maps==1&&get_unaligned_le64(out+4)==r.dva);set_bit(1,d.ext26_released);ext26_map_piodma(&d,201,out,in);assert(maps==1&&!get_unaligned_le32(out)&&d.memdesc[1].buf); + fragmented=true;r=dcpep_cb_allocate_buffer(&d,&q);assert(!r.mem_desc_id&&frees==1&&!test_bit(2,d.memdesc_map)); + free(d.memdesc[1].buf);puts("native D451 physically contiguous16K reply and D201 word1, packed16-byte reply, repeat mapping and logical retention");} +''') diff --git a/tools/asahi/dcpext26-present-tests.py b/tools/asahi/dcpext26-present-tests.py new file mode 100644 index 00000000000000..930c808a712b54 --- /dev/null +++ b/tools/asahi/dcpext26-present-tests.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Actual DRM primary start/submit callbacks: faults and early exact completion.""" +from pathlib import Path +import os,re,subprocess,tempfile +ROOT=Path(__file__).resolve().parents[2] +s=(ROOT/'drivers/gpu/drm/apple/iomfb_template.c').read_text() +def function(name): + m=re.search(r'^static void '+name+r'\(',s,re.M);assert m + return s[m.start():s.index('\n}',m.start())+2] +pre=r''' +#include +#include +#include +#include +#include +#include +typedef uint32_t u32;typedef uint64_t u64; +#define WRITE_ONCE(x,y) ((x)=(y)) +#define DCP_FW_NAME(n) n +#define DCP_FW_UNION(x) ((x).h17p) +#define dev_err(...) ((void)0) +#define trace_iomfb_swap_submit(...) ((void)0) +struct dc_swap_complete_resp { u32 swap_id; }; +struct dcp_swap_submit_resp { u32 ret; }; +struct dcp_swap_start_resp { u32 swap_id,ret; }; +struct apple_dcp { bool native,crashed,ext26_blocked,ext26_early_complete;u32 ext26_early_id; + u64 ext26_rx_ns,ext26_early_ns,swap_start,swap_submit_timestamp;void *dev,*crtc; + struct { struct { struct {u32 swap_id;} swap; } h17p; } swap; }; +static int submit_result;static bool begin_allowed=true; +static unsigned failures,events,armed,watchdogs,completed,sent;static u64 completion_time; +static bool dcp_is_ext26(struct apple_dcp *d) { return d->native; } +static int dcp_present_submit(struct apple_dcp *d,u32 id,bool ok) { (void)d;(void)id;(void)ok;return submit_result; } +static void dcp_present_failed(struct apple_dcp *d) { (void)d;failures++; } +static bool dcp_present_retires(struct apple_dcp *d) { (void)d;return true; } +static void dcp_unarm_retained_framebuffers(struct apple_dcp *d,u32 id) { (void)d;(void)id; } +static void dcp_drm_crtc_vblank(void *c) { (void)c;events++; } +static void dcpep_cb_swap_complete(struct apple_dcp *d,struct dc_swap_complete_resp *r) { assert(r->swap_id==d->swap.h17p.swap.swap_id);completed++;completion_time=d->ext26_rx_ns; } +static u64 ktime_get(void) { return 30; } +static u64 arch_timer_read_counter(void) { return 40; } +static void dcp_swap_watchdog_arm(struct apple_dcp *d) { (void)d;watchdogs++; } +static bool dcp_present_begin(struct apple_dcp *d,u32 id) { (void)d;assert(id);return begin_allowed; } +static void dcp_arm_retained_framebuffers(struct apple_dcp *d,u32 id) { (void)d;(void)id;armed++; } +static void dcp_h17p_prepare_swap(struct apple_dcp *d) { assert(!d->native); } +static void dcp_swap_submit(struct apple_dcp *d,bool oob,void *r,void (*cb)(struct apple_dcp *,void *,void *),void *c) { (void)d;(void)r;(void)cb;(void)c;assert(!oob);sent++; } +''' +check=r''' +static struct apple_dcp fresh(bool native) { + struct apple_dcp d={.native=native};d.swap.h17p.swap.swap_id=7; + failures=events=armed=watchdogs=completed=sent=0;submit_result=0;begin_allowed=true;return d; +} +int main(void) { + struct dcp_swap_submit_resp bad={1},ok={0};struct apple_dcp d=fresh(true); + dcp_swapped(&d,&bad,NULL);assert(d.crashed&&d.ext26_blocked&&failures==1&&events==1&&!watchdogs); + d=fresh(false);dcp_swapped(&d,&bad,NULL);assert(!d.crashed&&!d.ext26_blocked&&failures==1); + d=fresh(true);dcp_swapped(&d,NULL,NULL);assert(d.crashed&&d.ext26_blocked&&!watchdogs); + d=fresh(true);submit_result=-ECANCELED;dcp_swapped(&d,&ok,NULL);assert(d.crashed&&d.ext26_blocked); + d=fresh(true);d.ext26_early_complete=true;d.ext26_early_id=7;d.ext26_early_ns=15;d.ext26_rx_ns=20; + dcp_swapped(&d,&ok,NULL);assert(completed==1&&completion_time==15&&d.ext26_rx_ns==20&&!d.ext26_early_complete&&!watchdogs&&!d.crashed); + d=fresh(true);dcp_swapped(&d,&ok,NULL);assert(watchdogs==1&&!d.crashed&&!failures); + struct dcp_swap_start_resp start={8,1};d=fresh(true);dcp_swap_started(&d,&start,NULL);assert(d.crashed&&d.ext26_blocked&&!armed&&!sent); + d=fresh(false);dcp_swap_started(&d,&start,NULL);assert(!d.crashed&&!sent); + d=fresh(true);start=(struct dcp_swap_start_resp){0,0};dcp_swap_started(&d,&start,NULL);assert(d.crashed&&!sent); + d=fresh(true);start.swap_id=8;begin_allowed=false;dcp_swap_started(&d,&start,NULL);assert(d.crashed&&!sent&&!armed); + d=fresh(true);dcp_swap_started(&d,&start,NULL);assert(!d.crashed&&sent==1&&armed==1&&d.swap.h17p.swap.swap_id==8); + puts("PASS actual native DRM start/submit rejection, classic controls and original-time early D590 joining"); +} +''' +with tempfile.TemporaryDirectory() as t: + p=Path(t);(p/'check.c').write_text(pre+'\n'.join(function(n) for n in ['dcp_present_rejected','dcp_swapped','dcp_swap_started'])+check) + subprocess.run([os.environ.get('CC','clang'),'-std=gnu11','-O1','-Wall','-Wextra','-Werror','-Wno-unused-parameter','-fsanitize=address,undefined',str(p/'check.c'),'-o',str(p/'check')],check=True) + subprocess.run([str(p/'check')],check=True) diff --git a/tools/asahi/dcpext26-profile-tests.py b/tools/asahi/dcpext26-profile-tests.py new file mode 100644 index 00000000000000..ca8c29b4cff6e7 --- /dev/null +++ b/tools/asahi/dcpext26-profile-tests.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Native26 callback/startup/readiness helpers with a synthetic peer boundary.""" +from pathlib import Path +import os +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +source = (ROOT / 'drivers/gpu/drm/apple/iomfb_h17p.c').read_text() +header = (ROOT / 'drivers/gpu/drm/apple/iomfb.h').read_text() + + +def function(name): + start = source.index('static ', source.index(name) - 40) + # Find the signature's start without relying on the return type's line wrapping. + pos = source.index(name + '(') + start = source.rfind('static ', 0, pos) + return source[start:source.index('\n}', pos)+2] + + +def block(text, name): + start = text.index(name) + return text[start:text.index('\n};', start)+3] + + +pre = r''' +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; typedef uint32_t u32; typedef uint64_t u64; +#define GFP_KERNEL 0 +#define kzalloc_obj(v) calloc(1,sizeof(v)) +#define kfree free +#define READ_ONCE(v) (v) +#define WRITE_ONCE(v,x) ((v)=(x)) +#define dev_err(...) ((void)0) +#define dev_info(...) ((void)0) +#define dev_dbg(...) ((void)0) +static u32 get_unaligned_le32(const void *p) { const u8 *b=p; return (u32)b[0]|(u32)b[1]<<8|(u32)b[2]<<16|(u32)b[3]<<24; } +static void put_unaligned_le32(u32 v,void *p) { u8 *b=p;for(unsigned i=0;i<4;i++) b[i]=v>>(8*i); } +static void put_unaligned_le64(u64 v,void *p) { u8 *b=p;for(unsigned i=0;i<8;i++) b[i]=v>>(8*i); } +struct mock_connector { bool connected; }; +struct apple_dcp { struct mock_connector *connector; bool crashed, ext26_normal, ext26_power_sent; u64 ext26_rx_ns,ext26_power_ns,ext26_generation,typec_generation; int ext26_ready; }; +static unsigned withdrawn,invalidated,notifications; +static void dcp_modes_begin_attachment(struct apple_dcp *d) { (void)d;withdrawn++; } +static void apple_connector_invalidate_edid(struct mock_connector *c) { (void)c;invalidated++; } +static void dcp_queue_hotplug(struct mock_connector *c) { (void)c;notifications++; } +static void complete_all(int *c) { (*c)++; } +static bool iomfb_validate_callback_h17p(int t,u32 i,u32 o) { (void)t;(void)i;(void)o;return false; } +struct dcp_method_entry { const char *name; char tag[4],tag_h17g[4],tag_ext26[4]; }; +#define IOMFB_METHOD(t,n) [n]={#n,{t[0],t[1],t[2],t[3]},{0},{0}} +#define IOMFB_METHOD_H17(t,h,n) [n]={#n,{t[0],t[1],t[2],t[3]},{h[0],h[1],h[2],h[3]},{0}} +#define IOMFB_METHOD_EXT26(t,e,n) [n]={#n,{t[0],t[1],t[2],t[3]},{0},{e[0],e[1],e[2],e[3]}} +static unsigned count,done,fault,bad_status; +static void boot_done(struct apple_dcp *d,void *o,void *c) { (void)d;(void)o;(void)c;done++; } +static void dcp_push(struct apple_dcp *,bool,const struct dcp_method_entry *,u32,u32,void *,void (*)(struct apple_dcp *,void *,void *),void *); +''' +peer = r''' +static void dcp_push(struct apple_dcp *d,bool oob,const struct dcp_method_entry *m,u32 in,u32 out,void *input,void (*next)(struct apple_dcp *,void *,void *),void *cookie) { + const char *expected[]={"A389","A446","A029","A468","A000","A465"}; + const u32 ins[]={0,0,0,4,4,0},outs[]={0,4,0,0,4,4}; + assert(count<6 && !oob); + const char *tag=m->tag_ext26[0]?m->tag_ext26:m->tag; + assert(!memcmp(tag,expected[count],4)); + assert(in==ins[count] && out==outs[count]); + if(in) assert(get_unaligned_le32(input)==1); + u32 reply=count>=4 ? 1 : 0; + count++; + if (bad_status==count) reply=0; + next(d,fault==count?NULL:&reply,cookie); +} +int main(void) { + struct apple_dcp d={0};struct ext26_boot *b=calloc(1,sizeof(*b));b->one=1; + ext26_boot_next(&d,NULL,b);assert(done==1 && count==6 && !d.crashed); + for(fault=1;fault<=6;fault++) { + d=(struct apple_dcp){0};done=count=0;b=calloc(1,sizeof(*b));b->one=1; + ext26_boot_next(&d,NULL,b);assert(!done && d.crashed && count==fault); + } + fault=0; + for(bad_status=5;bad_status<=6;bad_status++) { + d=(struct apple_dcp){0};done=count=0;b=calloc(1,sizeof(*b));b->one=1; + ext26_boot_next(&d,NULL,b);assert(!done && d.crashed && count==bad_status); + } + unsigned char input[128]={0}, output[128]; + memset(output,0xcd,sizeof(output));ext26_bandwidth(&d,3,output+1,input); + for(unsigned i=0;i<60;i++) assert(output[i+1]==(i==56)); + assert(output[0]==0xcd && output[61]==0xcd); + for(unsigned i=0;i<80;i++) input[i]=i; + input[80]=0;ext26_frame_sync(&d,6,output,input);assert(!memcmp(output,input,80)); + input[80]=1;ext26_frame_sync(&d,6,output,input); + for(unsigned i=0;i<80;i++) assert(!output[i]); + memcpy(input,"VORP",4);put_unaligned_le32(0,input+4);ext26_clock(&d,408,output,input); + assert(get_unaligned_le32(output)==48000000); + put_unaligned_le32(1,input+4);ext26_clock(&d,408,output,input);assert(get_unaligned_le32(output)==24000000); + put_unaligned_le32(2,input+4);ext26_clock(&d,408,output,input);assert(!get_unaligned_le32(output)); + d=(struct apple_dcp){.ext26_power_sent=true,.ext26_power_ns=10,.ext26_generation=7,.typec_generation=7}; + memset(input,0,sizeof(input));memcpy(input,"NormalModeActive",17);input[64]=1; + for(unsigned t=0;t<=10;t++) { d.ext26_rx_ns=t;ext26_property(&d,104,NULL,input);assert(!d.ext26_normal && !d.ext26_ready); } + d.ext26_rx_ns=11;ext26_property(&d,104,NULL,input);assert(d.ext26_normal && d.ext26_ready==1); + input[64]=0;ext26_property(&d,104,NULL,input);assert(!d.ext26_normal); + input[64]=1;d.typec_generation++;ext26_property(&d,104,NULL,input);assert(!d.ext26_normal); + d.typec_generation--;d.ext26_power_sent=false;ext26_property(&d,104,NULL,input);assert(!d.ext26_normal); + d.ext26_power_sent=true;ext26_property(&d,104,NULL,input);assert(d.ext26_normal); + ext26_property(&d,107,NULL,input);assert(!d.ext26_normal); + struct mock_connector connector={true};d.connector=&connector; + memset(input,0,sizeof(input));memcpy(input,"TimingElements",15);ext26_property(&d,107,NULL,input); + assert(withdrawn==1&&invalidated==1&¬ifications==1&&!connector.connected); + const unsigned tags[]={3,6,107,115,116,134,408,454,567,572,576,588}; + const unsigned ins[]={4,84,64,16,16,0,8,4,128,4,4,0},outs[]={60,80,0,8,4,4,8,4,4,4,0,0}; + for(unsigned i=0;i deferred OPEN, and nested D121 transport regressions. + +Extract production C bodies. Firmware/queues are peers, not hardware proof. +An optional --capture JSON supplies bounded logged prefixes, never fabricated +complete captures. Unlogged dictionaries below are explicitly synthetic. +""" +from pathlib import Path +import argparse,json,os,re,subprocess,tempfile +R=Path(__file__).resolve().parents[2];A=R/'drivers/gpu/drm/apple' +def fn(s,name): + matches=list(re.finditer(r'(?m)^[^\n;{}]*\b'+name+r'\([^;{}]*\)\n\{',s)) + if not matches: raise RuntimeError('missing body '+name) + m=matches[0];start=m.start() + if s[start:m.end()].lstrip().startswith(name+'('):start=s.rfind('\n',0,start-1)+1 + end=m.end();depth=1 + while depth:depth+=(s[end]=='{')-(s[end]=='}');end+=1 + return s[start:end] +def block(s,name): + start=s.index(name);end=s.index('\n};',start)+3;return s[start:end] +def run(name,code,reject=False): + with tempfile.TemporaryDirectory() as t: + p=Path(t)/'test.c';p.write_text(code) + subprocess.run([os.environ.get('CC','clang'),'-std=gnu11','-Wall','-Wextra','-Werror','-Wno-unused-function','-Wno-unused-parameter','-Wno-sign-compare','-Wno-missing-field-initializers','-fsanitize=address,undefined',str(p),'-o',str(p.with_suffix(''))],check=True) + result=subprocess.run([str(p.with_suffix(''))],capture_output=reject,check=False) + if reject: + if result.returncode == 0:raise RuntimeError('mutant survived: '+name) + else:result.check_returncode() + print('PASS',name,flush=True) +args=argparse.ArgumentParser();args.add_argument('--capture',type=Path);args.add_argument('--mutants',action='store_true');args=args.parse_args() +pre=r''' +#include +#include +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8;typedef uint16_t u16;typedef uint32_t u32;typedef uint64_t u64;typedef int64_t s64;typedef uint32_t __le32; +#define __packed __attribute__((packed)) +#define GFP_KERNEL 0 +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) +#define ARRAY_SIZE(x) (sizeof(x)/sizeof(*(x))) +#define round_up(x,a) (((x)+(a)-1)&~((a)-1)) +#define ALIGN round_up +#define IS_ERR(p) ((uintptr_t)(p)>=(uintptr_t)-4095) +#define IS_ERR_OR_NULL(p) (!(p)||IS_ERR(p)) +#define PTR_ERR(p) ((int)(intptr_t)(p)) +#define ERR_PTR(e) ((void *)(intptr_t)(e)) +#define min_t(t,a,b) ((t)(a)<(t)(b)?(t)(a):(t)(b)) +#define BIT(n) (1UL<<(n)) +#define U32_MAX UINT32_MAX +#define S64_MIN INT64_MIN +#define S64_MAX INT64_MAX +#define kmalloc(n,f) malloc(n) +#define kzalloc_obj(v) calloc(1,sizeof(v)) +#define kfree(p) free((void *)(p)) +#define max(a,b) ((a)>(b)?(a):(b)) +#define dev_info(...) ((void)0) +#define dev_err(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define dev_warn(...) ((void)0) +#define dev_warn_ratelimited(...) ((void)0) +#define EXPORT_SYMBOL_IF_KUNIT(...) +#define spin_lock_irqsave(l,f) ((f)=0) +#define spin_unlock_irqrestore(l,f) ((void)(f)) +static void put_unaligned_le32(u32 v,void *p){memcpy(p,&v,4);} +static u32 get_unaligned_le32(const void *p){u32 v;memcpy(&v,p,4);return v;} +static u64 get_unaligned_le64(const void *p){u64 v;memcpy(&v,p,8);return v;} +''' +parser=(A/'parser.c').read_text();afk=(A/'afk.c').read_text();dptx=(A/'dptxep.c').read_text();dcp=(A/'dcp.c').read_text();h=(A/'afk.h').read_text() +codec=re.sub(r'^#include[^\n]*\n','',(A/'afkv2-codec.h').read_text(),flags=re.M) +identity=pre+codec+r''' +#define DCP_FIRMWARE_H17P 1 +#define DCP_FIRMWARE_V_14_7 2 +#define AFK_MAX_CHANNEL 16 +#define SYSTEM_ENDPOINT 0x20 +#define DISP0_ENDPOINT 0x23 +#define EPIC_TYPE_NOTIFY 0 +#define EPIC_CAT_REPORT 0 +#define bitmap_empty(p,n) 1 +#define MAX_PENDING_CMDS 16 +struct dcp_parse_ctx {void *dcp;const void *blob;u32 pos,len;}; +struct apple_epic_service;struct apple_dcp_afkep; +struct apple_epic_service_ops {const char *name;bool reusable,optional;int (*prepare)(struct apple_epic_service *,const char *,const char *,s64);int (*revalidate)(struct apple_epic_service *,const char *,const char *,s64);void (*init)(struct apple_epic_service *,const char *,const char *,s64);void (*teardown)(struct apple_epic_service *);}; +struct apple_epic_service {struct apple_dcp_afkep *ep;const struct apple_epic_service_ops *ops;bool enabled,torndown,pending_open;unsigned users;int lock;u32 channel;void *cookie;const char *pending_name,*pending_class;s64 pending_unit;int cmd_map[1];}; +struct dptx_port {struct apple_epic_service *service;s64 unit;bool enabled,connected;int enable_completion;}; +struct apple_dcp {bool native,ext26_services_open;int fw_compat,index,dptx_controller_ready;struct {unsigned num_dptx_ports;} hw;struct dptx_port dptxport[2];void *dev;struct apple_dcp_afkep *systemep,*ibootep,*dptx_controller_ep,*dcpavservep,*dptxep,*ext26_aux[3];}; +struct apple_dcp_afkep {struct apple_dcp *dcp;u32 endpoint,num_channels;bool wire_v2,stopping,match_epic_name;int lock;void *wq,*service_work;const struct apple_epic_service_ops *ops;struct apple_epic_service services[AFK_MAX_CHANNEL];}; +static bool dcp_is_ext26(struct apple_dcp *d){return d->native;} +static bool dcp_is_v14_7_external(struct apple_dcp *d){return false;} +static bool dcp_fw_is_14_7(struct apple_dcp *d){return d->fw_compat==DCP_FIRMWARE_V_14_7;} +static size_t strscpy(char *d,const void *v,size_t n){const char *s=v;size_t l=strnlen(s,n-1);memcpy(d,s,l);d[l]=0;return l;} +static void complete(int *c){(*c)++;} +#define trace_dptxport_init(...) +#define smp_store_release(p,v) (*(p)=(v)) +static unsigned opened[256][16],queued,published; +static int open_error; +static void afk_populate_service_debugfs(struct apple_epic_service *s){} +static void afk_recv_handle_teardown(struct apple_dcp_afkep *e,u32 c){} +static void afk_retry_service_init(struct apple_dcp_afkep *ep); +void afk_service_retry(struct apple_dcp_afkep *ep); +bool dptx_native_controllers_ready(struct apple_dcp *dcp); +static int queue_work(void *wq,void *work){struct apple_dcp_afkep *e=wq;assert(e->dcp->ext26_services_open||!e->stopping);queued++;afk_retry_service_init(e);return 1;} +static int afk_send_epic(struct apple_dcp_afkep *e,u32 ch,u16 tag,int ty,int cat,u8 sub,const void *p,size_t n){assert(sub==AFKV2_OPEN&&!n&&ch<16&&e->dcp->ext26_services_open);opened[e->endpoint][ch]++;return open_error;} +''' +identity+=parser[parser.index('#define DCP_PARSE_HEADER'):parser.index('static int parse_bool(')] +# Drop the audio-only helper region from the extracted prefix. +identity=re.sub(r'#if IS_ENABLED\(CONFIG_DRM_APPLE_AUDIO\).*?#endif','',identity,flags=re.S) +identity+=block(parser,'struct iterator {')+'\n'+fn(parser,'iterator_begin')+'\n'+fn(parser,'parse')+'\n'+fn(parser,'parse_epic_service_init')+'\n' +for name in ['afk_service_reclaimable','afk_service_reinit','afk_service_discard','afk_service_publish','afk_service_matches']: + identity+=fn(h,name)+'\n' +identity+=fn(dptx,'dptx_native_controllers_ready')+'\n'+fn(dptx,'dptx_native_controller_prepare')+'\n'+fn(dptx,'dptx_controller_service_init')+'\n' +for name in ['afk_match_service','afk_epic_find_service','afk_discard_pending_open','afk_retry_service_init','afk_service_retry']: + identity+=fn(afk,name)+'\n' +service=(A/'dptx-service.h').read_text() +for name in ['dptx_service_port','dptx_service_bind','dptx_service_publish']: + identity+=fn(service,name)+'\n' +identity+=fn(dptx,'dptxport_prepare')+'\n'+fn(dcp,'dcp_ext26_open_services')+'\n'+fn(afk,'afk_recv_handle_init')+r''' +static void port_init(struct apple_epic_service *s,const char *n,const char *c,s64 u){dptx_service_publish(s);published++;} +static const struct apple_epic_service_ops port_ops[]={{.name="AppleDCPDPTXRemotePort",.prepare=dptxport_prepare,.init=port_init},{0}}; +static const struct apple_epic_service_ops controller_ops[]={{.name="AppleDCPDPTXController",.prepare=dptx_native_controller_prepare,.init=dptx_controller_service_init},{0}}; +static const struct apple_epic_service_ops av_ops[]={{.name="DCPDPService"},{0}}; +struct blob {u8 p[2048];size_t n;}; +static void tag(struct blob *b,u32 t,u32 n){b->n=ALIGN(b->n,4);put_unaligned_le32(t<<24|n,b->p+b->n);b->n+=4;} +static void text(struct blob *b,const char *s){tag(b,9,strlen(s));memcpy(b->p+b->n,s,strlen(s));b->n+=strlen(s);} +static void number(struct blob *b,s64 v){tag(b,4,64);memcpy(b->p+b->n,&v,8);b->n+=8;} +static struct blob description(const char *name,const char *class,s64 unit,u32 channel,bool alias_first){ + struct blob b={0};memcpy(b.p,"dispext0",9);b.n=32;put_unaligned_le32(0xd3,b.p+b.n);b.n+=4;tag(&b,1,4); + if(alias_first){text(&b,"interface-id");number(&b,channel);} + text(&b,"EPICName");text(&b,name);text(&b,"EPICProviderClass");text(&b,class);text(&b,"EPICUnit");number(&b,unit); + if(!alias_first){text(&b,"interface-id");number(&b,channel);}return b; +} +static void announce(struct apple_dcp_afkep *e,unsigned ch,struct blob b){ + u8 record[4096];size_t size;struct afkv2_frame f; + assert(!afkv2_encode_frame(record,sizeof(record),0,ch,0,0x11,AFKV2_REPORT,4,b.p,b.n,&size)); + assert(!afkv2_decode_frame(record,size,false,&f));afk_recv_handle_init(e,f.interface,(void *)f.payload,f.payload_size); +} +int main(void){ + struct apple_dcp d={.native=true,.fw_compat=DCP_FIRMWARE_H17P,.index=1,.hw.num_dptx_ports=2};struct apple_dcp_afkep eps[8]={0}; + struct apple_dcp_afkep **slots[]={&d.systemep,&d.ibootep,&d.dptx_controller_ep,&d.ext26_aux[0],&d.dcpavservep,&d.ext26_aux[1],&d.dptxep,&d.ext26_aux[2]}; + const unsigned ids[]={0x20,0x23,0x24,0x27,0x28,0x29,0x2a,0x2c}; + for(unsigned i=0;i<8;i++){*slots[i]=&eps[i];eps[i]=(struct apple_dcp_afkep){.dcp=&d,.endpoint=ids[i],.wire_v2=true};eps[i].wq=&eps[i];} + d.dptxep->ops=port_ops;d.dptx_controller_ep->ops=controller_ops;d.dcpavservep->ops=av_ops; + // Infrastructure dictionaries have interface metadata but no EPIC identity, as on the working daily run. + struct blob infrastructure={0};memcpy(infrastructure.p,"system",7);infrastructure.n=32; + put_unaligned_le32(0xd3,infrastructure.p+32);infrastructure.n+=4;tag(&infrastructure,1,2); + text(&infrastructure,"interface-id");number(&infrastructure,1);text(&infrastructure,"ep-has-desc-mgr");tag(&infrastructure,11,1); + announce(d.systemep,1,infrastructure);announce(d.ibootep,1,infrastructure);assert(!d.systemep->num_channels&&!d.ibootep->num_channels); + // Both alias orders, distinct interface and EPICUnit, pass through production AFK/parser/prepare. + announce(d.dptxep,1,description("dcpdptx-port-epic","AppleDCPDPTXRemotePort",0,1,false)); + announce(d.dptxep,3,description("dcpdptx-port-epic","AppleDCPDPTXRemotePort",1,3,true)); + assert(d.dptxep->num_channels==2&&d.dptxport[0].unit==0&&d.dptxport[1].unit==1); + assert(!d.dptxport[0].service&&!d.dptxport[1].service); + for(unsigned i=0;i<4;i++)announce(d.dptx_controller_ep,2*i+1,description(i&1?"dcpdp-controller-epic":"dcpav-controller-epic","AppleDCPDPTXController",i/2,2*i+1,false)); + announce(d.dcpavservep,1,description("dcpav-service-epic","DCPDPService",0,1,false)); + // No service becomes visible and no OPEN goes out before A401. + assert(!published);for(unsigned i=0;i<8;i++)for(unsigned c=0;c<16;c++)assert(!opened[ids[i]][c]); + d.ext26_services_open=true;afk_service_retry(d.dptxep);assert(!published&&!opened[0x2a][1]); + queued=0;dcp_ext26_open_services(&d);assert(queued==9&&published==2&&dptx_native_controllers_ready(&d)); + assert(d.dptxport[0].service&&d.dptxport[1].service&&d.dptxport[0].enabled&&d.dptxport[1].enabled); + assert(opened[0x2a][1]==1&&opened[0x2a][3]==1&&opened[0x28][1]==1); + for(unsigned i=0;i<4;i++)assert(opened[0x24][2*i+1]==1); + dcp_ext26_open_services(&d);assert(published==2&&opened[0x2a][1]==1); + d.dptx_controller_ep->services[0].enabled=false;assert(!dptx_native_controllers_ready(&d));d.dptx_controller_ep->services[0].enabled=true; + // Native invalid units still fail closed; truncated identities publish nothing. + announce(d.dptxep,5,description("dcpdptx-port-epic","AppleDCPDPTXRemotePort",3,5,false));assert(!opened[0x2a][5]); + struct blob short_b={0};short_b.n=1;announce(d.dptxep,7,short_b);assert(!opened[0x2a][7]); + // Panel aliases retain their existing meaning; native explicit EPIC keys do not consume interface-id. + struct blob b=description("dcpdptx-port-epic","AppleDCPDPTXRemotePort",0,3,false);struct dcp_parse_ctx ctx;const char *n,*c;s64 u=-1; + assert(!parse(b.p+32,b.n-32,&ctx));assert(!parse_epic_service_init(&ctx,&n,&c,&u,false)&&u==0);free((void *)n);free((void *)c); + assert(!parse(b.p+32,b.n-32,&ctx));assert(!parse_epic_service_init(&ctx,&n,&c,&u,true)&&u==3);free((void *)n);free((void *)c); + // Preserve panel-only lowercase aliases, which native26 intentionally does not accept. + struct blob panel={0};put_unaligned_le32(0xd3,panel.p);panel.n=4;tag(&panel,1,3); + text(&panel,"compartment");text(&panel,"system");text(&panel,"name");text(&panel,"system");text(&panel,"interface-id");number(&panel,1); + assert(!parse(panel.p,panel.n,&ctx));assert(!parse_epic_service_init(&ctx,&n,&c,&u,true)&&u==1&&!strcmp(n,"system"));free((void *)n);free((void *)c); + assert(!parse(panel.p,panel.n,&ctx));assert(parse_epic_service_init(&ctx,&n,&c,&u,false)==-ENOENT); + // A failed OPEN cannot publish a prepared port; closed workqueues are never retried. + struct apple_dcp_afkep failed={.dcp=&d,.endpoint=0x2a,.wire_v2=true,.ops=port_ops};failed.wq=&failed; + d.dptxport[0].service=NULL;d.dptxport[0].enabled=false;open_error=-EIO;announce(&failed,1,description("dcpdptx-port-epic","AppleDCPDPTXRemotePort",0,1,false));assert(!d.dptxport[0].service&&!failed.services[0].enabled); + failed.stopping=true;unsigned q=queued;afk_service_retry(&failed);assert(queued==q); + d.native=false;failed.stopping=false;afk_service_retry(&failed);assert(queued==q); // classic only retries its EDID endpoint + for(unsigned i=0;i<8;i++)for(unsigned j=0;j A401 queues all eight endpoints -> exact once OPEN and publication; aliases, truncation, bad units, transport failure and classic retry guard"); +} +''' +run('native AFK announcements through deferred OPEN',identity) +# Compile the actual command allocator and ACK dispatch with the production D121 +# callback. A peer can write one Boolean byte or all four bytes, then nest D003. +iom=(A/'iomfb.c').read_text();native=(A/'iomfb_h17p.c').read_text();ih=(A/'iomfb.h').read_text();internal=(A/'dcp-internal.h').read_text() +transport=pre+r''' +#define DCP_FIRMWARE_H17P 1 +#define DCP_IOMFB_METHODS_H17G 2 +#define DCP_IOMFB_METHODS_H17P_EXTERNAL 3 +#define DCP_MAX_CALL_DEPTH 8 +#define IOMFB_MAX_CB 600 +#define DCP_PACKET_ALIGNMENT (0x40) +#define IOMFB_ENDPOINT 0x37 +#define IOMFB_MESSAGE_TYPE 15ULL +#define IOMFB_MESSAGE_TYPE_MSG 2 +#define IOMFB_MSG_CONTEXT 0xf00ULL +#define IOMFB_MSG_OFFSET 0xffff0000ULL +#define IOMFB_MSG_LENGTH 0xffffffff00000000ULL +#define IOMFB_MSG_ACK 64ULL +#define FIELD_PREP(m,v) (((u64)(v)<<__builtin_ctzll(m))&(m)) +#define FIELD_GET(m,v) (((v)&(m))>>__builtin_ctzll(m)) +#define WARN_ON(x) assert(!(x)) +#define IS_ENABLED(x) 0 +#define current ((void *)1) +#define dma_wmb() ((void)0) +#define dma_rmb() ((void)0) +#define trace_iomfb_push(...) +#define IOMFB_METHOD(t,n) [n]={#n,{t[0],t[1],t[2],t[3]},{0},{0}} +#define IOMFB_METHOD_H17(t,h,n) [n]={#n,{t[0],t[1],t[2],t[3]},{h[0],h[1],h[2],h[3]},{0}} +#define IOMFB_METHOD_EXT26(t,e,n) [n]={#n,{t[0],t[1],t[2],t[3]},{0},{e[0],e[1],e[2],e[3]}} +struct apple_dcp;typedef void (*dcp_callback_t)(struct apple_dcp *,void *,void *); +struct dcp_method_entry {const char *name;char tag[4],tag_h17g[4],tag_ext26[4];}; +''' +transport+=block(ih,'enum dcp_context_id {')+'\n'+block(ih,'struct dcp_packet_header {')+'\n'+block(internal,'struct dcp_channel {')+'\n'+block(ih,'enum dcpep_method {')+r''' +struct apple_dcp {void *dev;u8 *shmem;int fw_compat;bool native,crashed,quiescing,ext26_blocked,ext26_power_sent;u64 ext26_power_ns;struct {int iomfb_method_profile;}hw;struct {void *owner;} iomfb;struct dcp_channel ch_cb,ch_cmd,ch_oobcb,ch_oobcmd,ch_async,ch_oobasync;}; +static bool iomfb_uses_queue(struct apple_dcp *d){return true;} +static bool dcp_is_ext26(struct apple_dcp *d){return d->native;} +static int iomfb_queue_command(struct apple_dcp *d,bool o,const struct dcp_method_entry *m,u32 i,u32 n,void *p,dcp_callback_t cb,void *c){abort();} +static u64 ktime_get_ns(void){return 1;} +static void dcp_send_message(struct apple_dcp *,u8,u64); +static void dcp_ack(struct apple_dcp *,enum dcp_context_id); +static void dcp_push(struct apple_dcp *,bool,const struct dcp_method_entry *,u32,u32,void *,dcp_callback_t,void *); +static void boot_done(struct apple_dcp *,void *,void *); +static unsigned commands,callback_acks;static int bad_status,corrupt;static bool byte_reply;static u8 memory[0x100000]; +''' +for name in ['dcp_tx_offset','dcp_call_context','dcp_get_channel','dcp_packet_start','dcp_push_depth','dcp_pop_depth','dcpep_msg','dcpep_ack','dcp_push','dcp_ack','dcpep_handle_ack']: + code=fn(iom,name) + if name in ['dcp_push','dcp_ack']:code='static '+code + transport+=code+'\n' +transport+=block(native,'static const struct dcp_method_entry dcp_methods[')+'\n'+block(native,'struct ext26_boot {')+'\n' +for name in ['ext26_boot_next','ext26_boot_callback','ext26_bandwidth']: + transport+=fn(native,name)+'\n' +transport+=fn((A/'iomfb_template.c').read_text(),'boot_done')+r''' +static void callback(struct apple_dcp *d,int tag,unsigned in,unsigned out,unsigned off){ + struct dcp_channel *ch=&d->ch_cb;u8 *record=d->shmem+0x60000+off; + // Firmware-originated callback record; production handlers and ACK share this backing. + memset(record,0,12+in+out);struct dcp_packet_header *h=(void *)record;h->in_len=in;h->out_len=out; + if(tag==3){u8 raw[4]={0x00,0x0b,0x20,0xf1};memcpy(record+12,raw,4);} // daily D003 input, captured in full + unsigned depth=dcp_push_depth(&ch->depth);ch->output[depth]=record+12+in;ch->end[depth]=off+ALIGN(12+in+out,64); + bool ack=tag==121?ext26_boot_callback(d,tag,record+12+in,record+12):ext26_bandwidth(d,tag,record+12+in,record+12); + if(ack)dcp_ack(d,DCP_CONTEXT_CB); +} +static void dcp_send_message(struct apple_dcp *d,u8 ep,u64 message){ + assert(ep==IOMFB_ENDPOINT);enum dcp_context_id context=FIELD_GET(IOMFB_MSG_CONTEXT,message); + if(message&IOMFB_MSG_ACK){callback_acks++;return;} + unsigned offset=FIELD_GET(IOMFB_MSG_OFFSET,message);struct dcp_packet_header *h=(void *)(d->shmem+dcp_tx_offset(context)+offset);u8 *in=(u8 *)(h+1),*out=in+h->in_len; + const char *tags[]={"983A","644A","920A","864A","000A","564A"};unsigned inputs[]={0,0,0,4,4,0},outputs[]={0,4,0,0,4,4}; + assert(commands<6&&!memcmp(h->tag,tags[commands],4)&&h->in_len==inputs[commands]&&h->out_len==outputs[commands]); + assert(context==DCP_CONTEXT_CB&&offset>=64);if(h->in_len)assert(get_unaligned_le32(in)==1); + unsigned call=++commands; + if(call==5){callback(d,3,4,60,64);assert(get_unaligned_le32(d->shmem+0x60000+64+12+4+56)==1);} + if(h->out_len){u32 value=call>=5?1:0;if(bad_status==call)value=0;if(byte_reply)*out=value;else put_unaligned_le32(value,out);} + if(corrupt==call)h->tag[0]^=1; + dcpep_handle_ack(d,context); +} +int main(void){ + for(unsigned full=0;full<2;full++){ + memset(memory,0,sizeof(memory));struct apple_dcp d={.shmem=memory,.native=true,.fw_compat=1,.iomfb.owner=current};d.ch_cmd.depth=1;commands=callback_acks=0;byte_reply=!full; + callback(&d,121,0,4,0);assert(!d.crashed&&commands==6&&callback_acks==2&&!d.ch_cb.depth&&get_unaligned_le32(memory+0x60000+12)==1); + } + for(bad_status=5;bad_status<=6;bad_status++){ + memset(memory,0,sizeof(memory));struct apple_dcp d={.shmem=memory,.native=true,.fw_compat=1,.iomfb.owner=current};d.ch_cmd.depth=1;commands=callback_acks=0;byte_reply=true;callback(&d,121,0,4,0);assert(d.crashed&&commands==bad_status&&callback_acks==1&&d.ch_cb.depth==1); + } + bad_status=0; + for(corrupt=1;corrupt<=6;corrupt++){ + memset(memory,0,sizeof(memory));struct apple_dcp d={.shmem=memory,.native=true,.fw_compat=1,.iomfb.owner=current};d.ch_cmd.depth=1;commands=callback_acks=0;callback(&d,121,0,4,0);assert(d.crashed&&commands==corrupt); + } + puts("actual dcp_push/shared memory/ACK/D121 nested D003: Boolean byte and word replies pass, semantic refusal and corrupt command records stop successors and retain the session"); +} +''' +run('native D121 through actual shared-memory command and ACK bodies',transport) +if args.capture: + capture=json.loads(args.capture.read_text());assert capture['candidate_raw_messages_logged'] is False + for record in capture['daily_afk_prefixes']: + b=bytes.fromhex(record['hex']);assert b[:4]==b'IOP ' and len(b)==128 + declared=int.from_bytes(b[20:24],'little')+8 + assert len(b)-16dcp->fw_compat == DCP_FIRMWARE_H17P &&\s*!dcp_is_ext26\(ep->dcp\)', 'ep->dcp->fw_compat == DCP_FIRMWARE_H17P', identity) + if old_alias==identity:raise RuntimeError('identity mutation missed') + run('reject restored panel interface-id alias on native port',old_alias,True) + old_retry=identity.replace('(!dcp_is_ext26(ep->dcp) && ep != ep->dcp->dcpavservep)', 'ep != ep->dcp->dcpavservep') + if old_retry==identity:raise RuntimeError('retry mutation missed') + run('reject restored EDID-only deferred OPEN queue',old_retry,True) + old_padding=transport.replace('dcp_is_ext26(dcp) ? 0 : 0xff', '0xff') + if old_padding==transport:raise RuntimeError('padding mutation missed') + run('reject restored native all-ones Boolean reply padding',old_padding,True) diff --git a/tools/asahi/dcpext26-wire-tests.py b/tools/asahi/dcpext26-wire-tests.py new file mode 100644 index 00000000000000..ea465f215f33e4 --- /dev/null +++ b/tools/asahi/dcpext26-wire-tests.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Execute the production native26 encoder against independent byte fixtures.""" +import ctypes as C +import os +from pathlib import Path +import struct +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] +SIZE = 0x1BD8 + + +class Primary(C.Structure): + _fields_ = [("address", C.c_uint64), ("backing", C.c_uint64), + ("width", C.c_uint32), ("height", C.c_uint32), ("pitch", C.c_uint32)] + + +def fixture(layer, address, width, height, pitch, swap_id=17): + """Byte schema, including every zero byte, independent of C field writes.""" + fields = {0x98: struct.pack('\n#include \ntypedef uint8_t u8; typedef uint32_t u32; typedef uint64_t u64;\n', + 'errno.h': '#ifdef __linux__\n#include \n#else\n#include \n#endif\n', 'string.h': '#include \n', + 'unaligned.h': '''#include +#include +static inline void put_unaligned_le32(uint32_t v, void *p) { + unsigned char *b=p; for (unsigned i=0;i<4;i++) b[i]=v>>(8*i); +} +static inline void put_unaligned_le64(uint64_t v, void *p) { + unsigned char *b=p; for (unsigned i=0;i<8;i++) b[i]=v>>(8*i); +} +static inline uint32_t get_unaligned_le32(const void *p) { + const unsigned char *b=p; uint32_t v=0; + for (unsigned i=0;i<4;i++) { v|=(uint32_t)b[i]<<(8*i); } + return v; +} +''', + }.items(): + (p / 'linux' / name).write_text(body) + (p / 'test.c').write_text('''#include "dcpext26-wire.h" +int encode(void *b, size_t n, u32 id, unsigned layer, const struct dcpext26_primary *p) { + return dcpext26_primary_encode(b,n,id,layer,p); +} +int start(const void *r,size_t n,u32 *id) { return dcpext26_start_reply(r,n,id); } +int submit(const void *r,size_t n) { return dcpext26_submit_reply(r,n); } +''') + exe = p / 'wire.so' + subprocess.run([os.environ.get('CC', 'cc'), '-shared', '-fPIC', '-O2', + '-Wall', '-Wextra', '-Werror', '-I' + str(p), + '-I' + str(ROOT / 'drivers/gpu/drm/apple'), str(p / 'test.c'), + '-o', str(exe)], check=True) + cls.lib = C.CDLL(str(exe)) + cls.lib.encode.argtypes = [C.c_void_p, C.c_size_t, C.c_uint32, + C.c_uint, C.POINTER(Primary)] + cls.lib.start.argtypes = [C.c_void_p, C.c_size_t, C.POINTER(C.c_uint32)] + cls.lib.submit.argtypes = [C.c_void_p, C.c_size_t] + + @classmethod + def tearDownClass(cls): + cls.temp.cleanup() + + def test_complete_surface_records(self): + for layer in range(4): + for w, h, pitch in [(3840, 2160, 15360), (1920, 1080, 8192), (1, 1, 4)]: + p = Primary(0x12345678000, pitch * h + 4096, w, h, pitch) + b = C.create_string_buffer(SIZE + 2) + C.memset(b, 0xCD, SIZE + 2) + self.assertEqual(self.lib.encode(C.byref(b, 1), SIZE, 17, layer, C.byref(p)), 0) + self.assertEqual(b.raw[1:-1], fixture(layer, p.address, w, h, pitch)) + self.assertEqual(b.raw[0], 0xCD) + self.assertEqual(b.raw[-1], 0xCD) + + def test_null_surface(self): + b = C.create_string_buffer(SIZE) + self.assertEqual(self.lib.encode(b, SIZE, 17, 0, None), 0) + self.assertEqual(b.raw, fixture(0, 0, 0, 0, 0)) + + def test_invalid_inputs_preserve_buffer(self): + good = (0x100000, 33177600, 3840, 2160, 15360) + cases = [(0, *good[1:]), (good[0], 4, *good[2:]), + (*good[:2], 0, *good[3:]), (*good[:3], 0, good[4]), + (*good[:4], 15359), (*good[:4], 16), + (2**64-4, *good[1:]), (good[0], 2**64-1, 1, 2**32-1, 8)] + for values in cases: + b = C.create_string_buffer(b'\xcd' * SIZE, SIZE) + p = Primary(*values) + self.assertLess(self.lib.encode(b, SIZE, 17, 0, C.byref(p)), 0) + self.assertEqual(b.raw, b'\xcd' * SIZE) + p = Primary(*good) + for length, swap_id, layer in [(SIZE-1, 17, 0), (SIZE+1, 17, 0), + (SIZE, 0, 0), (SIZE, 17, 4), (SIZE, 17, 2**32-1)]: + b = C.create_string_buffer(b'\xcd' * SIZE, SIZE) + self.assertLess(self.lib.encode(b, length, swap_id, layer, C.byref(p)), 0) + self.assertEqual(b.raw, b'\xcd' * SIZE) + + def test_reply_lengths_and_status_offsets(self): + ident = C.c_uint32(0xFFFFFFFF) + for n in range(20): + start = C.create_string_buffer(struct.pack(' +#include +#include +#include + +#ifndef ASAHI_HEADER +#define ASAHI_HEADER "drm/asahi_drm.h" +#endif +#include ASAHI_HEADER + +static_assert(sizeof(struct drm_asahi_cmd_header) == 8, "command header"); +static_assert(sizeof(struct drm_asahi_attachment) == 24, "attachment"); +static_assert(sizeof(struct drm_asahi_timestamps) == 16, "timestamps"); +static_assert(offsetof(struct drm_asahi_cmd_compute, ts_end_copies) == 64, + "compute prefix"); +static_assert(offsetof(struct drm_asahi_cmd_render, ts_frag_end_copies) == 256, + "render prefix"); + +static int emit(unsigned int type, const void *payload, size_t size) +{ + struct drm_asahi_cmd_header header = { + .cmd_type = type, + .size = size, + .vdm_barrier = DRM_ASAHI_BARRIER_NONE, + .cdm_barrier = DRM_ASAHI_BARRIER_NONE, + }; + + return fwrite(&header, sizeof(header), 1, stdout) != 1 || + fwrite(payload, size, 1, stdout) != 1; +} + +/* Named C offsets are exported independently of the Rust decoder. */ +static void layout(void) +{ + printf("profile.params_required %zu 0\n", + offsetof(struct drm_asahi_params_global, gpu_hal_generation) + sizeof(__u32)); + printf("profile.vm_create_size %zu 0\n", sizeof(struct drm_asahi_vm_create)); + printf("profile.hal200 %u 0\n", (unsigned int)DRM_ASAHI_GPU_HAL_200); + printf("profile.features %u 0\n", + DRM_ASAHI_FEATURE_VM_STATUS | DRM_ASAHI_FEATURE_SCHEDULED_QUEUES); + printf("header %zu 0\n", sizeof(struct drm_asahi_cmd_header)); + printf("attachment %zu 0\n", sizeof(struct drm_asahi_attachment)); + printf("timestamps %zu 0\n", sizeof(struct drm_asahi_timestamps)); + printf("cmd_render.flags %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, flags), + sizeof(((struct drm_asahi_cmd_render *)0)->flags)); + printf("cmd_render.isp_zls_pixels %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_zls_pixels), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_zls_pixels)); + printf("cmd_render.vdm_ctrl_stream_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, vdm_ctrl_stream_base), + sizeof(((struct drm_asahi_cmd_render *)0)->vdm_ctrl_stream_base)); + printf("cmd_render.vertex_helper.binary %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, vertex_helper.binary), + sizeof(((struct drm_asahi_cmd_render *)0)->vertex_helper.binary)); + printf("cmd_render.vertex_helper.cfg %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, vertex_helper.cfg), + sizeof(((struct drm_asahi_cmd_render *)0)->vertex_helper.cfg)); + printf("cmd_render.vertex_helper.data %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, vertex_helper.data), + sizeof(((struct drm_asahi_cmd_render *)0)->vertex_helper.data)); + printf("cmd_render.fragment_helper.binary %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, fragment_helper.binary), + sizeof(((struct drm_asahi_cmd_render *)0)->fragment_helper.binary)); + printf("cmd_render.fragment_helper.cfg %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, fragment_helper.cfg), + sizeof(((struct drm_asahi_cmd_render *)0)->fragment_helper.cfg)); + printf("cmd_render.fragment_helper.data %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, fragment_helper.data), + sizeof(((struct drm_asahi_cmd_render *)0)->fragment_helper.data)); + printf("cmd_render.isp_scissor_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_scissor_base), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_scissor_base)); + printf("cmd_render.isp_dbias_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_dbias_base), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_dbias_base)); + printf("cmd_render.isp_oclqry_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_oclqry_base), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_oclqry_base)); + printf("cmd_render.depth.base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, depth.base), + sizeof(((struct drm_asahi_cmd_render *)0)->depth.base)); + printf("cmd_render.depth.comp_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, depth.comp_base), + sizeof(((struct drm_asahi_cmd_render *)0)->depth.comp_base)); + printf("cmd_render.depth.stride %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, depth.stride), + sizeof(((struct drm_asahi_cmd_render *)0)->depth.stride)); + printf("cmd_render.depth.comp_stride %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, depth.comp_stride), + sizeof(((struct drm_asahi_cmd_render *)0)->depth.comp_stride)); + printf("cmd_render.stencil.base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, stencil.base), + sizeof(((struct drm_asahi_cmd_render *)0)->stencil.base)); + printf("cmd_render.stencil.comp_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, stencil.comp_base), + sizeof(((struct drm_asahi_cmd_render *)0)->stencil.comp_base)); + printf("cmd_render.stencil.stride %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, stencil.stride), + sizeof(((struct drm_asahi_cmd_render *)0)->stencil.stride)); + printf("cmd_render.stencil.comp_stride %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, stencil.comp_stride), + sizeof(((struct drm_asahi_cmd_render *)0)->stencil.comp_stride)); + printf("cmd_render.zls_ctrl %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, zls_ctrl), + sizeof(((struct drm_asahi_cmd_render *)0)->zls_ctrl)); + printf("cmd_render.ppp_multisamplectl %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ppp_multisamplectl), + sizeof(((struct drm_asahi_cmd_render *)0)->ppp_multisamplectl)); + printf("cmd_render.sampler_heap %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, sampler_heap), + sizeof(((struct drm_asahi_cmd_render *)0)->sampler_heap)); + printf("cmd_render.ppp_ctrl %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ppp_ctrl), + sizeof(((struct drm_asahi_cmd_render *)0)->ppp_ctrl)); + printf("cmd_render.width_px %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, width_px), + sizeof(((struct drm_asahi_cmd_render *)0)->width_px)); + printf("cmd_render.height_px %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, height_px), + sizeof(((struct drm_asahi_cmd_render *)0)->height_px)); + printf("cmd_render.layers %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, layers), + sizeof(((struct drm_asahi_cmd_render *)0)->layers)); + printf("cmd_render.sampler_count %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, sampler_count), + sizeof(((struct drm_asahi_cmd_render *)0)->sampler_count)); + printf("cmd_render.utile_width_px %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, utile_width_px), + sizeof(((struct drm_asahi_cmd_render *)0)->utile_width_px)); + printf("cmd_render.utile_height_px %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, utile_height_px), + sizeof(((struct drm_asahi_cmd_render *)0)->utile_height_px)); + printf("cmd_render.samples %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, samples), + sizeof(((struct drm_asahi_cmd_render *)0)->samples)); + printf("cmd_render.sample_size_B %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, sample_size_B), + sizeof(((struct drm_asahi_cmd_render *)0)->sample_size_B)); + printf("cmd_render.isp_merge_upper_x %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_merge_upper_x), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_merge_upper_x)); + printf("cmd_render.isp_merge_upper_y %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_merge_upper_y), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_merge_upper_y)); + printf("cmd_render.bg.usc %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, bg.usc), + sizeof(((struct drm_asahi_cmd_render *)0)->bg.usc)); + printf("cmd_render.bg.rsrc_spec %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, bg.rsrc_spec), + sizeof(((struct drm_asahi_cmd_render *)0)->bg.rsrc_spec)); + printf("cmd_render.bg_rsrc_spec_hi %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, bg_rsrc_spec_hi), + sizeof(((struct drm_asahi_cmd_render *)0)->bg_rsrc_spec_hi)); + printf("cmd_render.eot.usc %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, eot.usc), + sizeof(((struct drm_asahi_cmd_render *)0)->eot.usc)); + printf("cmd_render.eot.rsrc_spec %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, eot.rsrc_spec), + sizeof(((struct drm_asahi_cmd_render *)0)->eot.rsrc_spec)); + printf("cmd_render.bg_eot_rsrc_spec_hi %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, bg_eot_rsrc_spec_hi), + sizeof(((struct drm_asahi_cmd_render *)0)->bg_eot_rsrc_spec_hi)); + printf("cmd_render.partial_bg.usc %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, partial_bg.usc), + sizeof(((struct drm_asahi_cmd_render *)0)->partial_bg.usc)); + printf("cmd_render.partial_bg.rsrc_spec %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, partial_bg.rsrc_spec), + sizeof(((struct drm_asahi_cmd_render *)0)->partial_bg.rsrc_spec)); + printf("cmd_render.bg_partial_rsrc_spec_hi %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, bg_partial_rsrc_spec_hi), + sizeof(((struct drm_asahi_cmd_render *)0)->bg_partial_rsrc_spec_hi)); + printf("cmd_render.partial_eot.usc %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, partial_eot.usc), + sizeof(((struct drm_asahi_cmd_render *)0)->partial_eot.usc)); + printf("cmd_render.partial_eot.rsrc_spec %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, partial_eot.rsrc_spec), + sizeof(((struct drm_asahi_cmd_render *)0)->partial_eot.rsrc_spec)); + printf("cmd_render.bg_eot_partial_rsrc_spec_hi %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, bg_eot_partial_rsrc_spec_hi), + sizeof(((struct drm_asahi_cmd_render *)0)->bg_eot_partial_rsrc_spec_hi)); + printf("cmd_render.isp_bgobjdepth %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_bgobjdepth), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_bgobjdepth)); + printf("cmd_render.isp_bgobjvals %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, isp_bgobjvals), + sizeof(((struct drm_asahi_cmd_render *)0)->isp_bgobjvals)); + printf("cmd_render.ts_vtx.start.handle %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_vtx.start.handle), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_vtx.start.handle)); + printf("cmd_render.ts_vtx.start.offset %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_vtx.start.offset), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_vtx.start.offset)); + printf("cmd_render.ts_vtx.end.handle %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_vtx.end.handle), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_vtx.end.handle)); + printf("cmd_render.ts_vtx.end.offset %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_vtx.end.offset), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_vtx.end.offset)); + printf("cmd_render.ts_frag.start.handle %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_frag.start.handle), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_frag.start.handle)); + printf("cmd_render.ts_frag.start.offset %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_frag.start.offset), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_frag.start.offset)); + printf("cmd_render.ts_frag.end.handle %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_frag.end.handle), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_frag.end.handle)); + printf("cmd_render.ts_frag.end.offset %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_frag.end.offset), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_frag.end.offset)); + printf("cmd_compute.flags %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, flags), + sizeof(((struct drm_asahi_cmd_compute *)0)->flags)); + printf("cmd_compute.sampler_count %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, sampler_count), + sizeof(((struct drm_asahi_cmd_compute *)0)->sampler_count)); + printf("cmd_compute.cdm_ctrl_stream_base %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, cdm_ctrl_stream_base), + sizeof(((struct drm_asahi_cmd_compute *)0)->cdm_ctrl_stream_base)); + printf("cmd_compute.cdm_ctrl_stream_end %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, cdm_ctrl_stream_end), + sizeof(((struct drm_asahi_cmd_compute *)0)->cdm_ctrl_stream_end)); + printf("cmd_compute.sampler_heap %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, sampler_heap), + sizeof(((struct drm_asahi_cmd_compute *)0)->sampler_heap)); + printf("cmd_compute.helper.binary %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, helper.binary), + sizeof(((struct drm_asahi_cmd_compute *)0)->helper.binary)); + printf("cmd_compute.helper.cfg %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, helper.cfg), + sizeof(((struct drm_asahi_cmd_compute *)0)->helper.cfg)); + printf("cmd_compute.helper.data %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, helper.data), + sizeof(((struct drm_asahi_cmd_compute *)0)->helper.data)); + printf("cmd_compute.ts.start.handle %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, ts.start.handle), + sizeof(((struct drm_asahi_cmd_compute *)0)->ts.start.handle)); + printf("cmd_compute.ts.start.offset %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, ts.start.offset), + sizeof(((struct drm_asahi_cmd_compute *)0)->ts.start.offset)); + printf("cmd_compute.ts.end.handle %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, ts.end.handle), + sizeof(((struct drm_asahi_cmd_compute *)0)->ts.end.handle)); + printf("cmd_compute.ts.end.offset %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, ts.end.offset), + sizeof(((struct drm_asahi_cmd_compute *)0)->ts.end.offset)); + printf("attachment.pointer %zu %zu\n", + offsetof(struct drm_asahi_attachment, pointer), + sizeof(((struct drm_asahi_attachment *)0)->pointer)); + printf("attachment.size %zu %zu\n", + offsetof(struct drm_asahi_attachment, size), + sizeof(((struct drm_asahi_attachment *)0)->size)); + printf("cmd_render.ts_frag_end_copies %zu %zu\n", + offsetof(struct drm_asahi_cmd_render, ts_frag_end_copies), + sizeof(((struct drm_asahi_cmd_render *)0)->ts_frag_end_copies)); + printf("cmd_compute.ts_end_copies %zu %zu\n", + offsetof(struct drm_asahi_cmd_compute, ts_end_copies), + sizeof(((struct drm_asahi_cmd_compute *)0)->ts_end_copies)); +} + +int main(int argc, char **argv) +{ + struct drm_asahi_attachment attachment = {0}; + + attachment.pointer = 0xcdb09376593c1f02ULL; + attachment.size = 0xceb194775a3d2003ULL; + + struct drm_asahi_cmd_render render = {0}; + + render.flags = 0x5b3e2104; + render.isp_zls_pixels = 0x5c3f2205; + render.vdm_ctrl_stream_base = 0xd1b4977a5d402306ULL; + render.vertex_helper.binary = 0x5e412407; + render.vertex_helper.cfg = 0x5f422508; + render.vertex_helper.data = 0xd4b79a7d60432609ULL; + render.fragment_helper.binary = 0x6144270a; + render.fragment_helper.cfg = 0x6245280b; + render.fragment_helper.data = 0xd7ba9d806346290cULL; + render.isp_scissor_base = 0xd8bb9e8164472a0dULL; + render.isp_dbias_base = 0xd9bc9f8265482b0eULL; + render.isp_oclqry_base = 0xdabda08366492c0fULL; + render.depth.base = 0xdbbea184674a2d10ULL; + render.depth.comp_base = 0xdcbfa285684b2e11ULL; + render.depth.stride = 0x694c2f12; + render.depth.comp_stride = 0x6a4d3013; + render.stencil.base = 0xdfc2a5886b4e3114ULL; + render.stencil.comp_base = 0xe0c3a6896c4f3215ULL; + render.stencil.stride = 0x6d503316; + render.stencil.comp_stride = 0x6e513417; + render.zls_ctrl = 0xe3c6a98c6f523518ULL; + render.ppp_multisamplectl = 0xe4c7aa8d70533619ULL; + render.sampler_heap = 0xe5c8ab8e7154371aULL; + render.ppp_ctrl = 0x7255381b; + render.width_px = 0x391c; + render.height_px = 0x3a1d; + render.layers = 0x3b1e; + render.sampler_count = 0x3c1f; + render.utile_width_px = 0x20; + render.utile_height_px = 0x21; + render.samples = 0x22; + render.sample_size_B = 0x23; + render.isp_merge_upper_x = 0x7b5e4124; + render.isp_merge_upper_y = 0x7c5f4225; + render.bg.usc = 0x7d604326; + render.bg.rsrc_spec = 0x7e614427; + render.bg_rsrc_spec_hi = 0x7f624528; + render.eot.usc = 0x80634629; + render.eot.rsrc_spec = 0x8164472a; + render.bg_eot_rsrc_spec_hi = 0x8265482b; + render.partial_bg.usc = 0x8366492c; + render.partial_bg.rsrc_spec = 0x84674a2d; + render.bg_partial_rsrc_spec_hi = 0x85684b2e; + render.partial_eot.usc = 0x86694c2f; + render.partial_eot.rsrc_spec = 0x876a4d30; + render.bg_eot_partial_rsrc_spec_hi = 0x886b4e31; + render.isp_bgobjdepth = 0x896c4f32; + render.isp_bgobjvals = 0x8a6d5033; + render.ts_vtx.start.handle = 0x8b6e5134; + render.ts_vtx.start.offset = 0x8c6f5235; + render.ts_vtx.end.handle = 0x8d705336; + render.ts_vtx.end.offset = 0x8e715437; + render.ts_frag.start.handle = 0x8f725538; + render.ts_frag.start.offset = 0x90735639; + render.ts_frag.end.handle = 0x9174573a; + render.ts_frag.end.offset = 0x9275583b; + + struct drm_asahi_cmd_compute compute = {0}; + + compute.flags = 0x9376593c; + compute.sampler_count = 0x94775a3d; + compute.cdm_ctrl_stream_base = 0xbeccfb295785b3eULL; + compute.cdm_ctrl_stream_end = 0xcedd0b396795c3fULL; + compute.sampler_heap = 0xdeed1b4977a5d40ULL; + compute.helper.binary = 0x987b5e41; + compute.helper.cfg = 0x997c5f42; + compute.helper.data = 0x10f1d4b79a7d6043ULL; + compute.ts.start.handle = 0x9b7e6144; + compute.ts.start.offset = 0x9c7f6245; + compute.ts.end.handle = 0x9d806346; + compute.ts.end.offset = 0x9e816447; + + size_t render_size = + offsetof(struct drm_asahi_cmd_render, ts_frag_end_copies); + size_t compute_size = + offsetof(struct drm_asahi_cmd_compute, ts_end_copies); + + if (argc == 2 && !strcmp(argv[1], "--layout")) { + layout(); + return 0; + } else if (argc == 2 && !strcmp(argv[1], "--full")) { + render_size = sizeof(render); + compute_size = sizeof(compute); + } else if (argc != 1) { + return 1; + } + + return emit(DRM_ASAHI_SET_VERTEX_ATTACHMENTS, &attachment, + sizeof(attachment)) || + emit(DRM_ASAHI_CMD_RENDER, &render, render_size) || + emit(DRM_ASAHI_CMD_COMPUTE, &compute, compute_size); +} diff --git a/tools/asahi/m3-uapi-tests.rs b/tools/asahi/m3-uapi-tests.rs new file mode 100644 index 00000000000000..4dc6012f5213e7 --- /dev/null +++ b/tools/asahi/m3-uapi-tests.rs @@ -0,0 +1,832 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +//! Offline tests of the production parser. All addresses are synthetic. +#![allow(dead_code)] + +#[path = "../../drivers/gpu/drm/asahi/m3/t8122_admission.rs"] +mod admission; +#[path = "../../drivers/gpu/drm/asahi/m3/m3_profile.rs"] +mod m3_profile; +#[path = "../../drivers/gpu/drm/asahi/m3/agx_uapi.rs"] +mod uapi; +use uapi::*; + +fn frame(kind: u16, payload: &[u8]) -> Vec { + let mut bytes = Vec::new(); + for word in [ + kind, + payload.len() as u16, + UAPI_BARRIER_NONE, + UAPI_BARRIER_NONE, + ] { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes.extend_from_slice(payload); + bytes +} + +fn parse(bytes: &[u8]) -> Result { + let mut parser = UapiCommandParser::new(bytes); + while parser.next_hardware()?.is_some() {} + parser.finish() +} + +// Each offset below is checked against offsetof of the corresponding C member. +fn layout() -> &'static std::collections::BTreeMap { + static LAYOUT: std::sync::OnceLock> = + std::sync::OnceLock::new(); + LAYOUT.get_or_init(|| { + let path = std::env::var("ASAHI_UAPI_LAYOUT").expect("C layout path"); + std::fs::read_to_string(path) + .unwrap() + .lines() + .map(|line| { + let words: Vec<_> = line.split_whitespace().collect(); + ( + words[0].to_string(), + (words[1].parse().unwrap(), words[2].parse().unwrap()), + ) + }) + .collect() + }) +} + +fn field(raw: &[u8], name: &str, offset: usize, width: usize, actual: u64) { + assert_eq!(layout()[name], (offset, width), "C offset/width for {name}"); + let mut expected = [0u8; 8]; + for (i, byte) in expected[..width].iter_mut().enumerate() { + *byte = raw.get(offset + i).copied().unwrap_or(0); + } + assert_eq!(actual, u64::from_le_bytes(expected), "decoded {name}"); +} + +fn check_render(p: UapiRenderCommand, raw: &[u8]) { + field(raw, "cmd_render.flags", 0, 4, p.flags as u64); + field( + raw, + "cmd_render.isp_zls_pixels", + 4, + 4, + p.depth_dimensions as u64, + ); + field( + raw, + "cmd_render.vdm_ctrl_stream_base", + 8, + 8, + p.vdm_base as u64, + ); + field( + raw, + "cmd_render.vertex_helper.binary", + 16, + 4, + p.vertex_helper.binary as u64, + ); + field( + raw, + "cmd_render.vertex_helper.cfg", + 20, + 4, + p.vertex_helper.config as u64, + ); + field( + raw, + "cmd_render.vertex_helper.data", + 24, + 8, + p.vertex_helper.data as u64, + ); + field( + raw, + "cmd_render.fragment_helper.binary", + 32, + 4, + p.fragment_helper.binary as u64, + ); + field( + raw, + "cmd_render.fragment_helper.cfg", + 36, + 4, + p.fragment_helper.config as u64, + ); + field( + raw, + "cmd_render.fragment_helper.data", + 40, + 8, + p.fragment_helper.data as u64, + ); + field( + raw, + "cmd_render.isp_scissor_base", + 48, + 8, + p.scissor_base as u64, + ); + field( + raw, + "cmd_render.isp_dbias_base", + 56, + 8, + p.depth_bias_base as u64, + ); + field( + raw, + "cmd_render.isp_oclqry_base", + 64, + 8, + p.occlusion_query_base as u64, + ); + field(raw, "cmd_render.depth.base", 72, 8, p.depth.base as u64); + field( + raw, + "cmd_render.depth.comp_base", + 80, + 8, + p.depth.compression_base as u64, + ); + field(raw, "cmd_render.depth.stride", 88, 4, p.depth.stride as u64); + field( + raw, + "cmd_render.depth.comp_stride", + 92, + 4, + p.depth.compression_stride as u64, + ); + field(raw, "cmd_render.stencil.base", 96, 8, p.stencil.base as u64); + field( + raw, + "cmd_render.stencil.comp_base", + 104, + 8, + p.stencil.compression_base as u64, + ); + field( + raw, + "cmd_render.stencil.stride", + 112, + 4, + p.stencil.stride as u64, + ); + field( + raw, + "cmd_render.stencil.comp_stride", + 116, + 4, + p.stencil.compression_stride as u64, + ); + field(raw, "cmd_render.zls_ctrl", 120, 8, p.zls_control as u64); + field( + raw, + "cmd_render.ppp_multisamplectl", + 128, + 8, + p.multisample_control as u64, + ); + field( + raw, + "cmd_render.sampler_heap", + 136, + 8, + p.sampler_heap as u64, + ); + field(raw, "cmd_render.ppp_ctrl", 144, 4, p.ppp_control as u64); + field(raw, "cmd_render.width_px", 148, 2, p.width as u64); + field(raw, "cmd_render.height_px", 150, 2, p.height as u64); + field(raw, "cmd_render.layers", 152, 2, p.layers as u64); + field( + raw, + "cmd_render.sampler_count", + 154, + 2, + p.sampler_count as u64, + ); + field( + raw, + "cmd_render.utile_width_px", + 156, + 1, + p.utile_width as u64, + ); + field( + raw, + "cmd_render.utile_height_px", + 157, + 1, + p.utile_height as u64, + ); + field(raw, "cmd_render.samples", 158, 1, p.samples as u64); + field( + raw, + "cmd_render.sample_size_B", + 159, + 1, + p.sample_size as u64, + ); + field( + raw, + "cmd_render.isp_merge_upper_x", + 160, + 4, + p.merge_upper_x as u64, + ); + field( + raw, + "cmd_render.isp_merge_upper_y", + 164, + 4, + p.merge_upper_y as u64, + ); + field(raw, "cmd_render.bg.usc", 168, 4, p.background.usc as u64); + field( + raw, + "cmd_render.bg.rsrc_spec", + 172, + 4, + (p.background.resource_spec & 0xffffffff) as u64, + ); + field( + raw, + "cmd_render.bg_rsrc_spec_hi", + 248, + 4, + (p.background.resource_spec >> 32) as u64, + ); + field(raw, "cmd_render.eot.usc", 176, 4, p.end_of_tile.usc as u64); + field( + raw, + "cmd_render.eot.rsrc_spec", + 180, + 4, + (p.end_of_tile.resource_spec & 0xffffffff) as u64, + ); + field( + raw, + "cmd_render.bg_eot_rsrc_spec_hi", + 240, + 4, + (p.end_of_tile.resource_spec >> 32) as u64, + ); + field( + raw, + "cmd_render.partial_bg.usc", + 184, + 4, + p.partial_background.usc as u64, + ); + field( + raw, + "cmd_render.partial_bg.rsrc_spec", + 188, + 4, + (p.partial_background.resource_spec & 0xffffffff) as u64, + ); + field( + raw, + "cmd_render.bg_partial_rsrc_spec_hi", + 252, + 4, + (p.partial_background.resource_spec >> 32) as u64, + ); + field( + raw, + "cmd_render.partial_eot.usc", + 192, + 4, + p.partial_end_of_tile.usc as u64, + ); + field( + raw, + "cmd_render.partial_eot.rsrc_spec", + 196, + 4, + (p.partial_end_of_tile.resource_spec & 0xffffffff) as u64, + ); + field( + raw, + "cmd_render.bg_eot_partial_rsrc_spec_hi", + 244, + 4, + (p.partial_end_of_tile.resource_spec >> 32) as u64, + ); + field( + raw, + "cmd_render.isp_bgobjdepth", + 200, + 4, + p.depth_clear as u64, + ); + field( + raw, + "cmd_render.isp_bgobjvals", + 204, + 4, + p.stencil_clear as u64, + ); + field( + raw, + "cmd_render.ts_vtx.start.handle", + 208, + 4, + p.vertex_timestamps.start.handle as u64, + ); + field( + raw, + "cmd_render.ts_vtx.start.offset", + 212, + 4, + p.vertex_timestamps.start.offset as u64, + ); + field( + raw, + "cmd_render.ts_vtx.end.handle", + 216, + 4, + p.vertex_timestamps.end.handle as u64, + ); + field( + raw, + "cmd_render.ts_vtx.end.offset", + 220, + 4, + p.vertex_timestamps.end.offset as u64, + ); + field( + raw, + "cmd_render.ts_frag.start.handle", + 224, + 4, + p.fragment_timestamps.start.handle as u64, + ); + field( + raw, + "cmd_render.ts_frag.start.offset", + 228, + 4, + p.fragment_timestamps.start.offset as u64, + ); + field( + raw, + "cmd_render.ts_frag.end.handle", + 232, + 4, + p.fragment_timestamps.end.handle as u64, + ); + field( + raw, + "cmd_render.ts_frag.end.offset", + 236, + 4, + p.fragment_timestamps.end.offset as u64, + ); +} + +fn check_compute(p: UapiComputeCommand, raw: &[u8]) { + field(raw, "cmd_compute.flags", 0, 4, p.flags as u64); + field( + raw, + "cmd_compute.sampler_count", + 4, + 4, + p.sampler_count as u64, + ); + field( + raw, + "cmd_compute.cdm_ctrl_stream_base", + 8, + 8, + p.control_stream_base as u64, + ); + field( + raw, + "cmd_compute.cdm_ctrl_stream_end", + 16, + 8, + p.control_stream_end as u64, + ); + field( + raw, + "cmd_compute.sampler_heap", + 24, + 8, + p.sampler_heap as u64, + ); + field( + raw, + "cmd_compute.helper.binary", + 32, + 4, + p.helper.binary as u64, + ); + field(raw, "cmd_compute.helper.cfg", 36, 4, p.helper.config as u64); + field(raw, "cmd_compute.helper.data", 40, 8, p.helper.data as u64); + field( + raw, + "cmd_compute.ts.start.handle", + 48, + 4, + p.timestamps.start.handle as u64, + ); + field( + raw, + "cmd_compute.ts.start.offset", + 52, + 4, + p.timestamps.start.offset as u64, + ); + field( + raw, + "cmd_compute.ts.end.handle", + 56, + 4, + p.timestamps.end.handle as u64, + ); + field( + raw, + "cmd_compute.ts.end.offset", + 60, + 4, + p.timestamps.end.offset as u64, + ); +} + +fn fixture() -> Vec { + std::fs::read(std::env::var("ASAHI_UAPI_FIXTURE").expect("C fixture path")).unwrap() +} + +#[test] +fn c_header_producer_matches_production_decoder() { + let offsets = layout(); + assert_eq!(offsets["header"].0, UAPI_COMMAND_HEADER_SIZE); + assert_eq!(offsets["attachment"].0, UAPI_ATTACHMENT_SIZE); + assert_eq!(offsets["timestamps"].0, 16); + assert_eq!(offsets["cmd_render.ts_frag_end_copies"].0, UAPI_RENDER_SIZE); + assert_eq!(offsets["cmd_compute.ts_end_copies"].0, UAPI_COMPUTE_SIZE); + let bytes = fixture(); + let render_offset = 2 * UAPI_COMMAND_HEADER_SIZE + UAPI_ATTACHMENT_SIZE; + let render_size = u16::from_le_bytes( + bytes[render_offset - 6..render_offset - 4] + .try_into() + .unwrap(), + ) as usize; + let compute_offset = render_offset + render_size + UAPI_COMMAND_HEADER_SIZE; + let mut parser = UapiCommandParser::new(&bytes); + let Some(ParsedHardwareCommand::Render { + index, + header, + payload, + vertex_attachments, + fragment_attachments, + }) = parser.next_hardware().unwrap() + else { + panic!("render missing") + }; + assert_eq!(index, 2); + assert_eq!( + header, + UapiCommandHeader { + command_type: 0, + size: render_size as u16, + render_barrier: UAPI_BARRIER_NONE, + compute_barrier: UAPI_BARRIER_NONE, + } + ); + check_render(payload, &bytes[render_offset..render_offset + render_size]); + assert_eq!(vertex_attachments.count, 1); + let attachment = vertex_attachments.as_slice()[0]; + field( + &bytes[8..32], + "attachment.pointer", + 0, + 8, + attachment.address, + ); + field(&bytes[8..32], "attachment.size", 8, 8, attachment.size); + assert!(fragment_attachments.as_slice().is_empty()); + let Some(ParsedHardwareCommand::Compute { + index, + header, + payload, + attachments, + }) = parser.next_hardware().unwrap() + else { + panic!("compute missing") + }; + assert_eq!(index, 3); + assert_eq!( + header, + UapiCommandHeader { + command_type: 1, + size: (bytes.len() - compute_offset) as u16, + render_barrier: UAPI_BARRIER_NONE, + compute_barrier: UAPI_BARRIER_NONE, + } + ); + check_compute(payload, &bytes[compute_offset..]); + assert!(attachments.as_slice().is_empty()); + assert!(parser.next_hardware().unwrap().is_none()); + assert_eq!(parser.finish(), Ok(2)); +} + +#[test] +fn short_payloads_zero_extend_but_missing_bytes_are_rejected() { + // Every length includes cuts within each multi-byte field, for both kinds. + for (kind, size) in [(0, 256), (1, 64)] { + let supplied: Vec = (0..size).map(|i| (i % 254 + 1) as u8).collect(); + for length in 0..=size { + let raw = &supplied[..length]; + let bytes = frame(kind, raw); + let mut parser = UapiCommandParser::new(&bytes); + match parser.next_hardware().unwrap().unwrap() { + ParsedHardwareCommand::Render { payload, .. } => check_render(payload, raw), + ParsedHardwareCommand::Compute { payload, .. } => check_compute(payload, raw), + } + assert!(parser.next_hardware().unwrap().is_none()); + assert_eq!(parser.finish(), Ok(1)); + for cut in 1..bytes.len() { + assert!(parse(&bytes[..cut]).is_err()); + } + } + } + assert_eq!(parse(&[]), Err(UapiTranslateError::HardwareCommandCount)); +} + +#[test] +fn named_optional_timestamp_copies_are_refused() { + for (kind, name) in [ + (0, "cmd_render.ts_frag_end_copies"), + (1, "cmd_compute.ts_end_copies"), + ] { + let (offset, size) = layout()[name]; + let mut raw = vec![0; offset + size]; + assert_eq!(parse(&frame(kind, &raw)), Ok(1)); + for i in offset..raw.len() { + raw[i] = 1; + assert_eq!( + parse(&frame(kind, &raw)), + Err(UapiTranslateError::PayloadTail), + "{name} byte {i}" + ); + raw[i] = 0; + } + } +} + +#[test] +fn unsupported_nonzero_extensions_never_disappear() { + for (kind, size) in [(0, UAPI_RENDER_SIZE), (1, UAPI_COMPUTE_SIZE)] { + for extra in [1, 8, 64, 128] { + let mut payload = vec![0; size + extra]; + assert_eq!(parse(&frame(kind, &payload)), Ok(1)); + for offset in size..payload.len() { + payload[offset] = 1; + assert_eq!( + parse(&frame(kind, &payload)), + Err(UapiTranslateError::PayloadTail) + ); + payload[offset] = 0; + } + } + } + assert_eq!( + parse(&frame(0xffff, &[])), + Err(UapiTranslateError::CommandType) + ); +} + +#[test] +fn attachment_shapes_counts_and_reserved_fields_are_checked() { + for kind in 2..=4 { + for size in 1..UAPI_ATTACHMENT_SIZE { + assert_eq!( + parse(&frame(kind, &vec![0; size])), + Err(UapiTranslateError::AttachmentPayloadSize) + ); + } + let mut bytes = frame(kind, &vec![0; UAPI_ATTACHMENT_SIZE * UAPI_MAX_ATTACHMENTS]); + bytes.extend(frame(1, &[])); + assert_eq!(parse(&bytes), Ok(1)); + assert_eq!( + parse(&frame( + kind, + &vec![0; UAPI_ATTACHMENT_SIZE * (UAPI_MAX_ATTACHMENTS + 1)] + )), + Err(UapiTranslateError::AttachmentCount) + ); + for offset in 16..24 { + let mut payload = [0; UAPI_ATTACHMENT_SIZE]; + payload[offset] = 1; + assert_eq!( + parse(&frame(kind, &payload)), + Err(UapiTranslateError::AttachmentFields) + ); + } + let mut bytes = frame(kind, &[]); + bytes[4..6].copy_from_slice(&0u16.to_le_bytes()); + assert_eq!(parse(&bytes), Err(UapiTranslateError::AttachmentBarrier)); + } +} + +#[test] +fn attachment_lists_reach_their_commands() { + let entry = |pointer: u64, size: u64| { + let mut raw = [0u8; UAPI_ATTACHMENT_SIZE]; + raw[..8].copy_from_slice(&pointer.to_le_bytes()); + raw[8..16].copy_from_slice(&size.to_le_bytes()); + raw + }; + let vertex = entry(0x1_2345_6000, 0x1_0000_0040).to_vec(); + let fragment = [ + entry(0x2_0000_8000, 0x80), + entry(0x3_0000_c000, 0x2_0000_0100), + ] + .concat(); + let compute = entry(0x4_0000_1000, 0x5_0000_0010).to_vec(); + let check = |list: UapiAttachmentList, raw: &[u8]| { + assert_eq!(list.as_slice().len(), raw.len() / UAPI_ATTACHMENT_SIZE); + for (attachment, raw) in list.as_slice().iter().zip(raw.chunks(UAPI_ATTACHMENT_SIZE)) { + field(raw, "attachment.pointer", 0, 8, attachment.address); + field(raw, "attachment.size", 8, 8, attachment.size); + } + }; + + // Lists persist until replaced; an empty list clears only its own kind. + let mut bytes = frame(2, &vertex); + bytes.extend(frame(3, &fragment)); + bytes.extend(frame(4, &compute)); + bytes.extend(frame(0, &[])); + bytes.extend(frame(1, &[])); + bytes.extend(frame(3, &[])); + bytes.extend(frame(0, &[])); + let mut parser = UapiCommandParser::new(&bytes); + for expected_fragment in [&fragment[..], &[]] { + let Some(ParsedHardwareCommand::Render { + vertex_attachments, + fragment_attachments, + .. + }) = parser.next_hardware().unwrap() + else { + panic!("render missing") + }; + check(vertex_attachments, &vertex); + check(fragment_attachments, expected_fragment); + if !expected_fragment.is_empty() { + let Some(ParsedHardwareCommand::Compute { attachments, .. }) = + parser.next_hardware().unwrap() + else { + panic!("compute missing") + }; + check(attachments, &compute); + } + } + assert!(parser.next_hardware().unwrap().is_none()); + assert_eq!(parser.finish(), Ok(3)); +} + +#[test] +fn dependencies_count_hardware_commands_and_bound_the_submit() { + for field in [4, 6] { + let mut bytes = frame(1, &[]); + bytes[field..field + 2].copy_from_slice(&1u16.to_le_bytes()); + assert_eq!(parse(&bytes), Err(UapiTranslateError::BarrierFuture)); + } + let mut bytes = frame(2, &[]); + bytes.extend(frame(0, &[])); + let mut compute = frame(1, &[]); + compute[4..6].copy_from_slice(&1u16.to_le_bytes()); + bytes.extend(compute); + assert_eq!(parse(&bytes), Ok(2)); + let command = frame(1, &[]); + assert_eq!( + parse(&command.repeat(UAPI_MAX_HARDWARE_COMMANDS as usize)), + Ok(UAPI_MAX_HARDWARE_COMMANDS) + ); + assert_eq!( + parse(&command.repeat(UAPI_MAX_HARDWARE_COMMANDS as usize + 1)), + Err(UapiTranslateError::HardwareCommandCount) + ); +} + +struct Region { + start: u64, + end: u64, + writable: bool, +} +impl GpuAddressSpace for Region { + fn covers(&self, address: u64, size: u64, access: GpuAccess) -> bool { + let Some(end) = address.checked_add(size) else { + return false; + }; + address >= self.start && end <= self.end && (access == GpuAccess::Read || self.writable) + } +} + +#[test] +fn attachment_validation_rejects_readonly_zero_and_wrapped_ranges() { + let mut list = UapiAttachmentList::EMPTY; + list.count = 1; + let mut region = Region { + start: 0x4000, + end: 0x8000, + writable: false, + }; + list.entries[0] = UapiAttachment { + address: 0x4000, + size: 0x4000, + }; + assert_eq!( + validate_attachments(®ion, &list), + Err(UapiTranslateError::AddressRange) + ); + region.writable = true; + assert_eq!(validate_attachments(®ion, &list), Ok(())); + for (address, size) in [(0x4000, 0), (0x4000, 0x4001), (u64::MAX, 2)] { + list.entries[0] = UapiAttachment { address, size }; + assert_eq!( + validate_attachments(®ion, &list), + Err(UapiTranslateError::AddressRange) + ); + } +} + +#[test] +fn usc_windows_reject_misalignment_overflow_and_unmapped_programs() { + let queue = QueueUscWindow { + base: USC_WINDOW_SIZE, + user_start: 0x4000, + user_end: 2 * USC_WINDOW_SIZE, + }; + assert_eq!(queue.validate(), Ok(())); + assert_eq!( + QueueUscWindow { + base: queue.base + 1, + ..queue + } + .validate(), + Err(UapiTranslateError::QueueUscAlignment) + ); + assert_eq!( + QueueUscWindow { + base: u64::MAX & !(USC_WINDOW_SIZE - 1), + ..queue + } + .validate(), + Err(UapiTranslateError::QueueUscRange) + ); + assert_eq!( + QueueUscWindow { + user_end: queue.user_end - 1, + ..queue + } + .validate(), + Err(UapiTranslateError::QueueUscRange) + ); + let region = Region { + start: queue.base + 8, + end: queue.base + 12, + writable: false, + }; + assert_eq!(queue.full_program(®ion, 9), Ok(queue.base + 8)); + assert_eq!( + queue.full_program(®ion, 0), + Err(UapiTranslateError::ProgramOffset) + ); + assert_eq!( + queue.full_program(®ion, 16), + Err(UapiTranslateError::AddressRange) + ); +} + +#[test] +fn unsupported_firmware_cannot_reach_the_existing_profile() { + let identity = (1, 1, 0x07022000, [0xff, 0, 0]); + assert!(admission::validate_identity( + &[14, 8, 3], + identity.0, + identity.1, + identity.2, + identity.3 + ) + .is_some()); + for version in [&[26, 6, 2][..], &[14, 8][..], &[14, 8, 3, 0][..], &[][..]] { + assert!(admission::validate_identity( + version, identity.0, identity.1, identity.2, identity.3 + ) + .is_none()); + } +} + +#[test] +fn firmware_profile_agrees_with_the_actual_c_header() { + assert_eq!( + layout()["profile.params_required"].0 as u64, + m3_profile::PARAMS_REQUIRED + ); + assert_eq!(layout()["profile.vm_create_size"].0, 24); + assert_eq!( + layout()["profile.hal200"].0 as u32, + m3_profile::Profile::Extended.hal() + ); + assert_eq!(layout()["profile.features"].0, 0x0a); +} diff --git a/tools/asahi/pmp-native26-tests.py b/tools/asahi/pmp-native26-tests.py new file mode 100644 index 00000000000000..5f9127ad21ad78 --- /dev/null +++ b/tools/asahi/pmp-native26-tests.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Execute native26 report admission on synthetic OF/tunable boundaries.""" +from pathlib import Path +import os +import re +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +source = (ROOT / 'drivers/pmdomain/apple/pmp-report.c').read_text() +start = source.index('static int apple_pmp_native_records(') +body = source[start:source.index('\n}', start)+2] +validation = re.sub(r'^#include .*\n', '', (ROOT / 'drivers/pmdomain/apple/pmp-report-validation.h').read_text(), flags=re.M) +pre = r''' +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; typedef uint32_t u32; typedef uint64_t u64; +#define BIT(x) (1U<<(x)) +#define BIT_ULL(x) (1ULL<<(x)) +#define __ffs64(x) __builtin_ctzll(x) +static u32 get_unaligned_le32(const void *p) { const u8 *b=p; return (u32)b[0]|(u32)b[1]<<8|(u32)b[2]<<16|(u32)b[3]<<24; } +static u64 get_unaligned_le64(const void *p) { return get_unaligned_le32(p)|(u64)get_unaligned_le32((const u8 *)p+4)<<32; } +static void put32(u32 v,void *p) { u8 *b=p; for(unsigned i=0;i<4;i++) b[i]=v>>(8*i); } +struct device_node { bool available; struct device_node *report; }; +struct device { struct device_node *of_node; }; +struct apple_pmp_report { struct device *dev; struct device_node *native_pmp; }; +static struct device_node chosen, report, suppliers[2]; +static bool board=true, native=true, optin=true, baduuid; +static int nodes=1, nt=20*0x7c, ng=8, nr=64; +static u8 table[20*0x7c], group[8], ranges[64]; +#define dev_info(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define for_each_compatible_node(np,t,c) for(int it=0;itavailable; } +static struct device_node *of_parse_phandle(struct device_node *p,const char *k,int i) { (void)k;(void)i;return p->report; } +static int of_property_read_string(struct device_node *p,const char *k,const char **s) { + (void)p;(void)k;*s=baduuid?"bad":"EFD60284-7C58-33A9-8522-CB3366AF6040";return 0; +} +static const void *of_get_property(struct device_node *p,const char *k,int *len) { + (void)p; + if (!strcmp(k,"asahi,j613-25g83-dcpext")) { *len=2;return optin?"1":NULL; } + if (!strcmp(k,"apple,tunable-soc-device")) { *len=nt;return table; } + if (!strcmp(k,"apple,tunable-soc-device-ps-group")) { *len=ng;return group; } + if (!strcmp(k,"apple,tunable-ptd-range")) { *len=nr;return ranges; } + return NULL; +} +''' +main = r''' +static void reset(void) { + board=native=optin=true;baduuid=false;nodes=1;nt=sizeof(table);ng=8;nr=64; + memset(table,0,sizeof(table));memset(group,0,sizeof(group));memset(ranges,0,sizeof(ranges)); + for(unsigned i=0;i<20;i++) put32(100+i,table+i*0x7c); + put32(8,table+11*0x7c);put32(9,table+12*0x7c);put32(18,table+19*0x7c); + put32(2,table+11*0x7c+8);put32(2,table+12*0x7c+8); + memcpy(table+11*0x7c+0x74,"DISP",5);memcpy(table+12*0x7c+0x74,"DISPEXT",8); + memcpy(table+19*0x7c+0x74,"ANS",4);put32(0x3fff0,group); + put32(10,ranges);put32(256,ranges+4);put32(8,ranges+8); + put32(11,ranges+32);put32(264,ranges+36);put32(8,ranges+40); + for(unsigned i=0;i<2;i++) { suppliers[i].available=true;suppliers[i].report=&report; } +} +static int admit(void) { struct device d={.of_node=&report};struct apple_pmp_report r={.dev=&d};return apple_pmp_native_records(&r); } +int main(void) { + reset();assert(!admit()); + reset();optin=false;board=false;assert(!admit()); /* Off leaves the old path intact. */ + reset();native=false;assert(!admit()); + reset();board=false;assert(admit()); + reset();nodes=0;assert(admit()); + reset();nodes=2;assert(admit()); + reset();baduuid=true;assert(admit()); + reset();suppliers[0].available=false;assert(admit()); + reset();nt--;assert(admit()); + reset();ng--;assert(admit()); + reset();nr--;assert(admit()); + reset();group[0]^=1;assert(admit()); + for(unsigned i=0;i<20;i++) { + if(i==11 || i==12 || i==19) continue; + reset();put32(8,table+i*0x7c);assert(admit()); + } + const unsigned positions[]={11,12,19}; + for(unsigned i=0;i<3;i++) { + reset();table[positions[i]*0x7c+0x74]^=1;assert(admit()); + reset();table[positions[i]*0x7c+8]^=2;assert(admit()); + } + reset();put32(257,ranges+4);assert(admit()); + puts("PASS native26 PTD personality, unique ordinals, acknowledgment and range refusals"); + return 0; +} +''' +with tempfile.TemporaryDirectory() as tmp: + p = Path(tmp) / 'native.c' + p.write_text(pre + validation + body + main) + binary = p.with_suffix('') + subprocess.run([os.environ.get('CC', 'cc'), '-std=gnu11', '-Wall', '-Wextra', '-Werror', + '-Wno-unused-function', '-O2', str(p), '-o', str(binary)], check=True) + subprocess.run([str(binary)], check=True) diff --git a/tools/asahi/t6030-pmp-graph-tests.c b/tools/asahi/t6030-pmp-graph-tests.c index d7d079a9673a64..93b21cc493c92b 100644 --- a/tools/asahi/t6030-pmp-graph-tests.c +++ b/tools/asahi/t6030-pmp-graph-tests.c @@ -23,6 +23,7 @@ typedef uint8_t u8; #define PMP_REPORT_READY 1 #define GFP_KERNEL 0 #define GENPD_FLAG_ACTIVE_WAKEUP 1 +#define GENPD_FLAG_ALWAYS_ON 2 #define MAX_PHANDLE_ARGS 4 #define EPROBE_DEFER 517 #define SZ_4K 4096 @@ -103,6 +104,8 @@ struct apple_pmp_report { struct list_head list; void *debugfs; struct apple_pmp_start *start; + struct device_node *native_pmp; + u64 native_claimed; }; struct completion { bool done; @@ -143,6 +146,7 @@ struct apple_pmp_report_entry { u32 id; bool no_ack, removed; bool sleep_release, released; + struct apple_pmp_report *native_owner; }; #define genpd_to_apple_pmp_report_entry(p) \ container_of(p, struct apple_pmp_report_entry, genpd) @@ -584,6 +588,13 @@ static const struct apple_pmp_start_ops apple_pmp_start_hw_ops = { hw_check, hw_bound }; static const struct apple_pmp_start_timing apple_pmp_start_timing = { 2, 0, 2, 2 }; +/* Native26 table admission is exercised by its separate actual-body fixture. */ +static int apple_pmp_native_records(struct apple_pmp_report *rep) +{ + assert(!rep->native_pmp); + return 0; +} + #include "production.h" static const struct apple_pmp_report_offsets * of_device_get_match_data(struct device *d) diff --git a/tools/asahi/t6030-pmp-graph-tests.py b/tools/asahi/t6030-pmp-graph-tests.py index 8757ee04cde1d6..61737534128f9b 100644 --- a/tools/asahi/t6030-pmp-graph-tests.py +++ b/tools/asahi/t6030-pmp-graph-tests.py @@ -33,7 +33,7 @@ def main(): entries.append((reg, 'apple,always-on;' in node, 'apple,no-ack;' in node, 'apple,defer-startup-ack;' in node)) assert len(entries) == 4 entry_data = ',\n'.join('{%d, %d, %d, %d}' % e for e in entries) - names = ['static int apple_pmp_report_bit(', 'static int apple_pmp_start_check_layout(', + names = ['static int apple_pmp_report_record(', 'static int apple_pmp_report_bit(', 'static int apple_pmp_start_check_layout(', 'static int apple_pmp_start_describe(', 'static void apple_pmp_start_finish(', 'static bool apple_pmp_start_stopping(', 'static void apple_pmp_start_work(', 'static void apple_pmp_start_drain(', 'static int apple_pmp_start_init(', @@ -83,8 +83,8 @@ def with_t8122(text): controls = [ ('no-managed-owner', '.starts_pmp = IS_ENABLED(CONFIG_APPLE_T6030_GPU_PMP_LINK)', '.starts_pmp = false'), ('no-managed-probe', 'ret = apple_pmp_start_init(rep);', 'ret = 0; (void)apple_pmp_start_init;'), - ('no-child-seeding', 'apple_pmp_report_entry_set_state(&ent->genpd, true);', '(void)0;'), - ('no-storage-policy', 'ent->no_ack = of_property_read_bool(node, "apple,no-ack");', 'ent->no_ack = false;'), + ('no-child-seeding', 'apple_pmp_report_entry_set_state(&ent->genpd, true);', '0;'), + ('no-storage-policy', 'ent->no_ack = of_property_read_bool(node, "apple,no-ack");\n\t}', 'ent->no_ack = false;\n\t}'), ('external-startup-ack', '!of_property_read_bool(child, "apple,defer-startup-ack")', 'true'), ('ignore-seeded-state', 'status || request != st->seed', 'status'), ('ignore-halted-state', 'status || request != st->seed', 'request != st->seed'), diff --git a/tools/asahi/t6030-pmp-supplier-tests.c b/tools/asahi/t6030-pmp-supplier-tests.c index 40e6be5bf24c57..5dd0345029c47f 100644 --- a/tools/asahi/t6030-pmp-supplier-tests.c +++ b/tools/asahi/t6030-pmp-supplier-tests.c @@ -47,6 +47,7 @@ struct apple_pmp_report_offsets { const char *name, *fallback, *pwrstate; }; struct apple_pmp_report { + struct device_node *native_pmp; void *base; const struct apple_pmp_report_offsets *offsets; struct device *dev; @@ -84,6 +85,14 @@ static void rcu_read_unlock(void) { assert(rcu_depth-- == 1); } +static int apple_pmp_report_record(struct device_node *pmp, + struct device_node *entry, u32 *id, bool *record) +{ + (void)pmp; + *id = entry->reg; + *record = false; + return 0; +} static int apple_pmp_report_bit(struct apple_pmp_start *st, struct device_node *entry, u32 *id, bool *record) diff --git a/tools/asahi/test-j613-dcp-power.py b/tools/asahi/test-j613-dcp-power.py new file mode 100644 index 00000000000000..3bc1a3379c253e --- /dev/null +++ b/tools/asahi/test-j613-dcp-power.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Compile the production J613 PMGR predicate and test PMGR fields and unrelated bits.""" +from pathlib import Path +import os +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +source = (root / "drivers/gpu/drm/apple/j613_dcp_activate.c").read_text() +start = source.index("static bool j613_power_state_active(") +end = source.index("\n}\n", start) + 3 +stub = r""" +#include +#include +#include +typedef uint32_t u32; +#define BIT(n) (1U << (n)) +#define GENMASK(a,b) (((1U << ((a)-(b)+1))-1) << (b)) +#define FIELD_GET(mask,v) (((v) & (mask)) >> __builtin_ctz(mask)) +""" +main = r""" +int main(void) { + /* Only ACTUAL[7:4], TARGET[3:0] and AUTO_ENABLE[28] affect readiness. */ + const u32 unrelated_mask = ~(GENMASK(7, 0) | BIT(28)); + u32 unrelated[32] = {0, unrelated_mask, + 0xaaaaaaaaU & unrelated_mask, + 0x55555555U & unrelated_mask}; + unsigned int count = 4; + for (unsigned int bit = 8; bit < 32; bit++) + if (bit != 28) unrelated[count++] = BIT(bit); + for (unsigned int i = 0; i < count; i++) + for (u32 actual = 0; actual < 16; actual++) + for (u32 target = 0; target < 16; target++) + for (u32 automatic = 0; automatic < 2; automatic++) { + u32 word = unrelated[i] | (actual << 4) | target | (automatic << 28); + bool expected = actual == 15 || (target == 15 && automatic); + if (j613_power_state_active(word) != expected) { + fprintf(stderr, "PMGR state 0x%08x: expected active=%d\n", word, expected); + return 1; + } + } + if (!j613_power_state_active(0x1404024f)) return 2; + if (j613_power_state_active(0x0404024f)) return 3; + if (j613_power_state_active(0x14040240)) return 4; + puts("PASS: 13824 PMGR combinations and captured-state regression controls"); + return 0; +} +""" +with tempfile.TemporaryDirectory() as temporary: + path = Path(temporary) / "power.c" + path.write_text(stub + source[start:end] + main) + for flags in (["-O0"], ["-O2"]): + exe = str(path.with_suffix("")) + subprocess.run([os.environ.get("CC", "cc"), "-Wall", "-Wextra", "-Werror", + *flags, str(path), "-o", exe], check=True) + subprocess.run([exe], check=True) diff --git a/tools/asahi/test-m3-mailbox.py b/tools/asahi/test-m3-mailbox.py new file mode 100644 index 00000000000000..167b0566b2e5dd --- /dev/null +++ b/tools/asahi/test-m3-mailbox.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Exercise the production T8122 mailbox predicate with captured DTB properties. + +Usage: python3 tools/asahi/test-m3-mailbox.py [runtime.dtb daily.dtb] +Requires dtc's fdtget. No target access or MMIO. +""" +import hashlib +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +board = (ROOT / 'drivers/gpu/drm/asahi/m3/m3_board.rs').read_text() +soc = (ROOT / 'drivers/gpu/drm/asahi/m3/m3_soc.rs').read_text() +t8122 = soc[soc.index('pub(crate) static T8122:'):] +compatibles = re.search(r'mailbox_compatibles: &(\[.*?\]),', t8122, re.S)[1] +irqs = re.search(r'mailbox_interrupts: &(\[.*?\]),', t8122, re.S)[1] +base = re.search(r'mailbox: (0x[0-9a-fA-F_]+),', t8122)[1] +regfn = re.search(r'fn expected_mailbox_reg\(.*?\n}', board, re.S)[0] +predicate = board[board.index(' if !soc.mailbox_compatibles'):] +predicate = predicate[:predicate.index('\n {')].strip()[3:] + + +def prop(dtb, node, name, kind): + result = subprocess.check_output(['fdtget', '-t', kind, str(dtb), node, name], text=True) + return [int(v, 16) for v in result.split()] + + +def vec(values): + return 'vec![' + ','.join(map(str, values)) + ']' + + +fixtures = [] +for argument in sys.argv[1:]: + dtb = Path(argument) + gpu = '/soc/gpu@292400000' + # Both retained captures use this mailbox path; verify the phandle link, + # not its capture-specific numeric value. + mbox = '/soc/mbox@292408000' + if prop(dtb, gpu, 'mboxes', 'x') != prop(dtb, mbox, 'phandle', 'x'): + raise RuntimeError('GPU mailbox phandle mismatch') + values = [prop(dtb, mbox, name, kind) for name, kind in + [('compatible', 'bx'), ('reg', 'x'), ('interrupt-names', 'bx'), + ('interrupts', 'x'), ('#mbox-cells', 'x')]] + if len(values[4]) != 1: + raise RuntimeError('bad #mbox-cells length') + fixtures.append('check(' + ','.join(vec(v) for v in values[:4]) + ',' + str(values[4][0]) + ');') + print(dtb.name, hashlib.sha256(dtb.read_bytes()).hexdigest()) +if not sys.argv[1:]: + # Public t8122-j613-dcp-25g83.dtsi and t8122-gpu.dtsi resource contract. + names = list(b'send-empty\0send-not-empty\0recv-empty\0recv-not-empty\0') + for compat in (b'apple,t8122-agx-asc-mailbox\0apple,asc-mailbox-v4\0', + b'apple,t8122-asc-mailbox\0apple,asc-mailbox-v4\0'): + values = [list(compat), [2, 0x92408000, 0, 0x4000], names, + [0,723,4,0,724,4,0,725,4,0,726,4]] + fixtures.append('check(' + ','.join(vec(v) for v in values) + ',0);') +if len(fixtures) != 2: + raise SystemExit('supply runtime.dtb and daily.dtb') + +source = ''' +struct Windows { mailbox: u64 } +struct Soc { mailbox_compatibles: &'static [&'static [u8]], mailbox_interrupts: &'static [u32], windows: Windows } +''' + regfn + ''' +fn accepts(soc: &Soc, compat: &Vec, mbox_reg: &Vec, irq_names: &Vec, irqs: &Vec, cells: u32) -> bool { + !(''' + predicate + ''') +} +fn check(compat: Vec, reg: Vec, names: Vec, irqs: Vec, cells: u32) { + let soc = Soc { mailbox_compatibles: &''' + compatibles + ''', mailbox_interrupts: &''' + irqs + ''', windows: Windows { mailbox: ''' + base + ''' } }; + let ok = |c: &Vec, r: &Vec, n: &Vec, i: &Vec, x| accepts(&soc,c,r,n,i,x); + assert!(ok(&compat,®,&names,&irqs,cells)); + for i in 0..reg.len() { let mut v=reg.clone(); v[i]^=1; assert!(!ok(&compat,&v,&names,&irqs,cells)); } + for i in 0..irqs.len() { let mut v=irqs.clone(); v[i]^=1; assert!(!ok(&compat,®,&names,&v,cells)); } + assert!(!ok(&compat,®,&names,&irqs,1)); + let mut v=names.clone(); v[0]^=1; assert!(!ok(&compat,®,&v,&irqs,cells)); + for v in [b"apple,asc-mailbox-v4\\0".to_vec(), b"apple,t8122-agx-asc-mailbox\\0unknown\\0".to_vec(), b"apple,t6031-agx-asc-mailbox\\0".to_vec()] { assert!(!ok(&v,®,&names,&irqs,cells)); } + // Compatible lists are ordered: reversing either supported pair must fail. + for pair in soc.mailbox_compatibles { + let parts: Vec<_> = pair.split(|byte| *byte == 0).filter(|part| !part.is_empty()).collect(); + if parts.len() == 2 { + let mut reversed = parts[1].to_vec(); + reversed.push(0); + reversed.extend_from_slice(parts[0]); + reversed.push(0); + assert!(!ok(&reversed,®,&names,&irqs,cells)); + } + } + for v in soc.mailbox_compatibles { assert!(ok(&v.to_vec(),®,&names,&irqs,cells)); } +} +fn main() { +''' + '\n'.join(fixtures) + '\nprintln!("mailbox: both captures, all exact supported lists and resource/identity mutation controls PASS");\n}\n' +with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) + (path / 'test.rs').write_text(source) + subprocess.run([os.environ.get('RUSTC', 'rustc'), '--edition=2021', str(path / 'test.rs'), '-o', str(path / 'test')], check=True) + subprocess.run([str(path / 'test')], check=True) +print('m3_board.rs', hashlib.sha256(board.encode()).hexdigest()) +print('m3_soc.rs', hashlib.sha256(soc.encode()).hexdigest()) diff --git a/tools/asahi/test-m3-power.py b/tools/asahi/test-m3-power.py new file mode 100644 index 00000000000000..51bea088cd9db5 --- /dev/null +++ b/tools/asahi/test-m3-power.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Run the production controller writer and soft-float code without a GPU. + +Kernel allocation/DT adapters are replaced by host adapters. Public production +power types and PStateTables are extracted as complete balanced Rust items, +never restated by hand. Their input hashes are printed with the test evidence. +""" +from pathlib import Path +import hashlib +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +M3 = ROOT / "drivers/gpu/drm/asahi/m3" + + +def item(source, anchor): + start = source.index(anchor) + opening = source.index("{", start) + depth = 1 + end = opening + 1 + while depth: + if source[end] == "{": + depth += 1 + elif source[end] == "}": + depth -= 1 + end += 1 + return source[start:end] + + +shim = r''' +#![allow(dead_code)] +extern crate self as kernel; +pub type Error = i32; +pub type Result = core::result::Result; +pub const EINVAL: Error = 22; +pub const EOVERFLOW: Error = 75; +pub const ERANGE: Error = 34; +pub const GFP_KERNEL: () = (); +pub unsafe trait Zeroable {} +#[derive(Debug)] +pub struct KVec(Vec); +impl Default for KVec { fn default() -> Self { Self(Vec::new()) } } +impl KVec { + pub fn new() -> Self { Self::default() } + pub fn resize(&mut self, n: usize, value: T, _: ()) -> Result where T: Clone { + self.0.resize(n, value); Ok(()) + } +} +impl core::ops::Deref for KVec { type Target = Vec; fn deref(&self) -> &Vec { &self.0 } } +impl core::ops::DerefMut for KVec { fn deref_mut(&mut self) -> &mut Vec { &mut self.0 } } +pub mod prelude { pub use crate::{Error,Result,EINVAL,EOVERFLOW,ERANGE,GFP_KERNEL,KVec,Zeroable}; } +pub mod of { + pub struct Property<'a>(pub &'a [u8]); + pub trait PropertyUnit: Sized { const UNIT_SIZE: usize; fn from_bytes(data: &[u8]) -> crate::Result; } + impl PropertyUnit for u32 { + const UNIT_SIZE: usize = 4; + fn from_bytes(data: &[u8]) -> crate::Result { + Ok(u32::from_be_bytes(data.try_into().map_err(|_| crate::EINVAL)?)) + } + } + impl TryFrom> for u32 { + type Error = crate::Error; + fn try_from(value: Property<'_>) -> crate::Result { Self::from_bytes(value.0) } + } +} +''' +fixture = r''' +#[cfg(test)] mod controller_tests { + use super::*; + use float::F32; + fn input() -> hw::PwrConfig { + let mut p = hw::PwrConfig::default(); + p.perf_states = KVec((0..9).map(|i| hw::PState { + freq_hz: if i == 8 { 1_338_000_000 } else { i * 100_000_000 }, + volt_mv: KVec(vec![if i == 0 { 125 } else { 450 + i * 50 }]), pwr_mw: i * 1000, + }).collect()); + p.perf_base_pstate = 1; + p.power_sample_period = 8; + p.ppm_filter_time_constant_ms = 16; + p.avg_power_filter_tc_ms = 16; + p.pwr_filter_time_constant = 2; + p.perf_filter_time_constant = 2; + p.perf_filter_time_constant2 = 2; + p.avg_power_target_filter_tc = 2; + p.se_filter_time_constant = 2; + p.se_filter_time_constant_1 = 2; + p.pwr_min_duty_cycle = 30; + p.avg_power_min_duty_cycle = 20; + p + } + fn build(p: &hw::PwrConfig, sram: &[u32]) -> Result { + m3_power_26::PowerImages::new(p, sram, 0xffff_fc21_0340_0000, + 0xffff_fc21_0350_0000, 0xffff_fc2f_fc00_0000, &[0; 53 * 40]) + } + fn word(b: &[u8], at: usize) -> u32 { u32::from_le_bytes(b[at..at + 4].try_into().unwrap()) } + #[test] fn native_controller_bounds_are_distinct_from_outputs_and_hardware_state() { + let image = build(&input(), &[950_001; 9]).unwrap(); + assert_eq!(image.hardware.len(), 0x2658); + assert_eq!(image.performance.len(), 0x6578); + assert_eq!(image.power.len(), 0x670); + assert_eq!(image.globals.len(), 0xe48); + assert_eq!(image.maximum_khz, 1_338_000); + assert_eq!(word(&image.performance, 0x2c), 1); + assert_eq!(word(&image.performance, 0x30), 1); + assert_eq!(word(&image.performance, 0x38), 0); + assert_eq!(word(&image.performance, 0x9f0), 30); + assert_eq!(word(&image.performance, 0xa38), 800); + assert_eq!(word(&image.performance, 0xb78), 100); + assert_eq!(word(&image.performance, 0xb7c), 800); + assert_eq!(word(&image.hardware, 0x1408 + 64), 951); + assert_eq!(word(&image.performance, 0x2268), 0x4248); + assert_eq!(word(&image.performance, 0x3b58), 0x4b48); + } + #[test] fn malformed_tunables_and_nonfinite_gains_are_rejected() { + for period in [0, 1001, u32::MAX] { + let mut p = input(); p.power_sample_period = period; + assert!(build(&p, &[950_000; 9]).is_err()); + } + for bits in [0x7f800000, 0xff800000, 0x7fc00001] { + let mut p = input(); p.pwr_integral_gain = F32::from_bits(bits); + assert!(build(&p, &[950_000; 9]).is_err()); + } + assert!(build(&input(), &[950_000; 8]).is_err()); + assert!(build(&input(), &[100_000; 9]).is_err()); + let mut p = input(); p.avg_power_target_filter_tc = u32::MAX; + assert!(build(&p, &[950_000; 9]).is_err()); + } + #[test] fn dt_order_is_not_the_firmware_voltage_order() { + let p = input(); + let expected = build(&p, &[950_000; 9]).unwrap(); + let mut shuffled = input(); + shuffled.perf_states.swap(2, 6); + let actual = build(&shuffled, &[950_000; 9]).unwrap(); + assert_eq!(actual.hardware.0, expected.hardware.0); + assert_eq!(actual.performance.0, expected.performance.0); + } +} +''' + + +def main(): + hw = (M3 / "hw.rs").read_text() + init = (M3 / "initdata.rs").read_text() + code = shim + # Keep the production soft-float prefix verbatim. Its unrelated existing + # print-only TODO test lacks an import and is not part of this harness. + soft_float = (ROOT / "drivers/gpu/drm/asahi/float.rs").read_text() + code += "mod float {\n" + soft_float.split("// TODO: Make this an actual test", 1)[0] + "\n}\n" + code += "mod hw { use crate::{KVec,float::F32};\n" + for name in ["PState", "PowerZone", "CsAfrPwrConfig", "PwrConfig"]: + code += "#[derive(Default)]\n" + item(hw, "pub(crate) struct " + name + " {") + "\n" + code += item(hw, "impl PState {") + "\n" + code += "}\nmod initdata { use crate::{prelude::*,hw}; const PSTATE_TABLE_ENTRIES: usize = 16;\n" + code += item(init, "pub(crate) struct PStateTables {") + "\n" + code += item(init, "impl PStateTables {") + "\n}\n" + code += "mod m3_mmio_26 { pub const SLOTS:usize=53; pub const RECORD_BYTES:usize=40; }\n" + code += f'#[path = "{M3}/m3_power_26.rs"] mod m3_power_26;\n' + code += fixture + for path in [M3 / "hw.rs", M3 / "initdata.rs", M3 / "m3_power_26.rs", ROOT / "drivers/gpu/drm/asahi/float.rs"]: + print(hashlib.sha256(path.read_bytes()).hexdigest(), path.relative_to(ROOT), flush=True) + with tempfile.TemporaryDirectory(prefix="m3-power-") as directory: + source = Path(directory) / "test.rs" + source.write_text(code) + for flags in [[], ["-O"]]: + binary = Path(directory) / "tests" + subprocess.run(["rustc", "--edition=2021", "--test", *flags, str(source), "-o", str(binary)], check=True) + subprocess.run([str(binary)], check=True) + + +if __name__ == "__main__": + main() diff --git a/tools/asahi/test-m3-uapi.py b/tools/asahi/test-m3-uapi.py new file mode 100755 index 00000000000000..a63249e18869d4 --- /dev/null +++ b/tools/asahi/test-m3-uapi.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Compile public C producers and run the production Rust parser offline.""" +import argparse +import hashlib +import os +from pathlib import Path +import subprocess +import sys +import tempfile + + +def run(command, **kwargs): + subprocess.run(command, check=True, **kwargs) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--mesa", type=Path, help="matched public Mesa source") + args = parser.parse_args() + root = Path(__file__).resolve().parents[2] + if sys.byteorder != "little": + raise SystemExit("C fixtures currently require a little-endian host") + cc = os.environ.get("CC", "cc") + rustc = os.environ.get("RUSTC", "rustc") + fixture_source = root / "tools/asahi/m3-uapi-fixture.c" + with tempfile.TemporaryDirectory(prefix="asahi-uapi-") as temporary: + work = Path(temporary) + headers = work / "headers" + run(["make", "-s", "-C", str(root), "O=" + str(work / "headers-build"), + "ARCH=arm64", "headers_install", "INSTALL_HDR_PATH=" + str(headers)]) + sources = [(headers / "include", "drm/asahi_drm.h")] + if args.mesa: + sources.append((args.mesa.resolve() / "include", "drm-uapi/asahi_drm.h")) + fixtures = [] + layouts = [] + prefixes = [] + for index, (includes, header) in enumerate(sources): + producer = work / ("producer-" + str(index)) + run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror", + "-I" + str(includes), '-DASAHI_HEADER="' + header + '"', + str(fixture_source), "-o", str(producer)]) + prefix = work / ("prefix-" + str(index)) + with prefix.open("wb") as output: + run([str(producer)], stdout=output) + prefixes.append(prefix) + layout = work / ("layout-" + str(index)) + with layout.open("wb") as output: + run([str(producer), "--layout"], stdout=output) + layouts.append(layout) + fixture = work / ("fixture-" + str(index)) + with fixture.open("wb") as output: + run([str(producer), "--full"], stdout=output) + fixtures.append(fixture) + reference = prefixes[0].read_bytes() + if any(prefix.read_bytes() != reference for prefix in prefixes[1:]): + raise SystemExit("kernel/Mesa canonical fixture mismatch") + print("canonical fixture sha256=" + hashlib.sha256(reference).hexdigest(), flush=True) + for optimized in (False, True): + binary = work / ("tests-optimized" if optimized else "tests-debug") + command = [rustc, "--edition=2021", "--test", + str(root / "tools/asahi/m3-uapi-tests.rs"), "-o", str(binary)] + if optimized: + command.append("-O") + run(command) + for index, fixture in enumerate(prefixes + fixtures): + env = dict(os.environ, ASAHI_UAPI_FIXTURE=str(fixture), + ASAHI_UAPI_LAYOUT=str(layouts[index % len(layouts)])) + run([str(binary)], env=env) + + +if __name__ == "__main__": + main() diff --git a/tools/aurora-ci/host-tests.json b/tools/aurora-ci/host-tests.json index a1b93d695dfd6e..cc9985e3ae4ec1 100644 --- a/tools/aurora-ci/host-tests.json +++ b/tools/aurora-ci/host-tests.json @@ -1,2319 +1,2469 @@ { - "comment": [ - "Hermetic host test suites, run by host-tests.py from the top of the tree.", - "{output} is a fresh path that does not exist yet; its parent does.", - "expect: pass (exit 0, the default) or fail (non-zero; mutation checks that report a caught mutant that way).", - "known_failure: runs, is reported, and does not fail the job; remove it once the suite passes.", - "commits: history the suite reads; host-tests.py --fetch-commits fetches it into a shallow checkout.", - "Every test-like file under the discovery roots is listed in suites or excluded (tests/test_host_tests.py checks).", - "helpers: files that look like suites but are run by a listed suite.", - "serial: the suite measures time; it runs alone, after the parallel batch.", - "group: the CI job that runs the suite; aurora-sep-slow holds the longest aurora-sep suites so they run beside the rest." - ], - "apt": [ - "bison", - "bubblewrap", - "clang", - "dbus", - "device-tree-compiler", - "flex", - "libarchive-tools", - "libegl-dev", - "libgles-dev", - "libnotify-bin", - "lld", - "llvm", - "make", - "makepkg", - "python3-gi", - "zstd" - ], - "discovery": [ - "tools/asahi", - "tools/asahi-m3", - "tools/aurora-*", - "tools/testing/apple", - "tools/testing/selftests/drivers/apple", - "tools/testing/selftests/drivers/gpu/apple-*", - "tools/testing/selftests/drivers/gpu/asahi-g15" - ], - "suites": [ - { - "name": "tools/asahi/apple-dart-fw-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/apple-dart-fw-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/apple-dart-range-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/apple-dart-range-tests.py" - ], - "known_failure": "its mocks predate the current apple-dart.c (no u32, fw_mirror/fw_handoff, apple_dart_fw_invalidate; apple_dart_hw_sync_locked takes three arguments now)" - }, - { - "name": "tools/asahi/avd-codec-tail-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/avd-codec-tail-tests.py", - "--mutations", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/avd-av1-tail-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/avd-av1-tail-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/avd-av1-tail-tests.py --mutation round-up", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/avd-av1-tail-tests.py", - "--out", - "{output}", - "--mutation", - "round-up" - ], - "expect": "fail", - "note": "inverted: exit status is the controls program status, non-zero when the mutant is caught" - }, - { - "name": "tools/asahi/avd-av1-tail-tests.py --mutation cpu-unaligned", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/avd-av1-tail-tests.py", - "--out", - "{output}", - "--mutation", - "cpu-unaligned" - ], - "expect": "fail", - "note": "inverted: exit status is the controls program status, non-zero when the mutant is caught" - }, - { - "name": "tools/asahi/bcm4377-id-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-id-tests.py", - "--out", - "{output}", - "--old-ref", - "87ddae1397f6f70817cb57df76477a86bdf96424" - ], - "commits": [ - "87ddae1397f6f70817cb57df76477a86bdf96424" - ] - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py --mutation epoch", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}", - "--mutation", - "epoch" - ], - "expect": "pass", - "note": "self-checking: exits 0 when the mutant aborts" - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py --mutation close", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}", - "--mutation", - "close" - ], - "expect": "pass", - "note": "self-checking: exits 0 when the mutant aborts" - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py --mutation producer-fence", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}", - "--mutation", - "producer-fence" - ], - "expect": "pass", - "note": "self-checking: exits 0 when the mutant aborts" - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py --mutation active-fence", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}", - "--mutation", - "active-fence" - ], - "expect": "pass", - "note": "self-checking: exits 0 when the mutant aborts" - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py --mutation board", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}", - "--mutation", - "board" - ], - "expect": "pass", - "note": "self-checking: exits 0 when the mutant aborts" - }, - { - "name": "tools/asahi/bcm4377-resume-tests.py --old-ref 36fab84538bed0f2f823c0e6745e4b0f5e6c2a1e", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/bcm4377-resume-tests.py", - "--out", - "{output}", - "--old-ref", - "36fab84538bed0f2f823c0e6745e4b0f5e6c2a1e" - ], - "expect": "pass", - "note": "self-checking: original driver (87ddae1397f6^) must abort cases 0,1,2,28", - "commits": [ - "36fab84538bed0f2f823c0e6745e4b0f5e6c2a1e" - ] - }, - { - "name": "tools/asahi/brcmf-assoc-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-assoc-tests.py", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/brcmf-assoc-tests.py --mutation short-ie", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-assoc-tests.py", - "--output", - "{output}", - "--mutation", - "short-ie" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-assoc-tests.py --mutation header-length", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-assoc-tests.py", - "--output", - "{output}", - "--mutation", - "header-length" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-assoc-tests.py --mutation skb-capacity", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-assoc-tests.py", - "--output", - "{output}", - "--mutation", - "skb-capacity" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-assoc-tests.py --mutation cmplt-header", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-assoc-tests.py", - "--output", - "{output}", - "--mutation", - "cmplt-header" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-assoc-tests.py --old-association f58ae50192550e6fc67c956f361f525881a1f969", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-assoc-tests.py", - "--output", - "{output}", - "--old-association", - "f58ae50192550e6fc67c956f361f525881a1f969" - ], - "expect": "fail", - "note": "inverted; old getter from ecd1ea4d0f06^", - "commits": [ - "f58ae50192550e6fc67c956f361f525881a1f969" - ] - }, - { - "name": "tools/asahi/brcmf-bss-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-bss-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/brcmfmac-scan-timeout-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmfmac-scan-timeout-tests.py", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/brcmfmac-scan-timeout-tests.py --old-source {output}/old.c", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmfmac-scan-timeout-tests.py", - "--output", - "{output}", - "--old-source", - "{output}/old.c" - ], - "expect": "pass", - "note": "self-checking; cfg80211.c from f58ae5019255^", - "setup": [ - [ - "sh", - "-c", - "mkdir -p \"$1\" && git show 661c0a5740e8f36816f7e79a784c36c026097368:drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c > \"$1/old.c\"", - "sh", - "{output}" - ] - ], - "commits": [ - "661c0a5740e8f36816f7e79a784c36c026097368" - ] - }, - { - "name": "tools/asahi/brcmf-pcie-dma-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-pcie-dma-tests.py", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/brcmf-pcie-dma-tests.py --mutation narrow-stream", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-pcie-dma-tests.py", - "--output", - "{output}", - "--mutation", - "narrow-stream" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-pcie-dma-tests.py --mutation ignore-aperture", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-pcie-dma-tests.py", - "--output", - "{output}", - "--mutation", - "ignore-aperture" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-pcie-dma-tests.py --mutation revision-wide", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-pcie-dma-tests.py", - "--output", - "{output}", - "--mutation", - "revision-wide" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/brcmf-pcie-dma-tests.py --old-dma 654386a164a1e93063f879491f10bebfc1439291", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-pcie-dma-tests.py", - "--output", - "{output}", - "--old-dma", - "654386a164a1e93063f879491f10bebfc1439291" - ], - "expect": "fail", - "note": "inverted; probe from dbdeefb63947^", - "commits": [ - "654386a164a1e93063f879491f10bebfc1439291" - ] - }, - { - "name": "tools/asahi/brcmf-pcie-dma-tests.py --firmware-revision b0de24a8c59735f21023c93592dd9a2eb9e155a4", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-pcie-dma-tests.py", - "--output", - "{output}", - "--firmware-revision", - "b0de24a8c59735f21023c93592dd9a2eb9e155a4" - ], - "expect": "fail", - "note": "inverted; firmware table from 2d17f269503c^", - "commits": [ - "b0de24a8c59735f21023c93592dd9a2eb9e155a4" - ] - }, - { - "name": "tools/asahi/brcmf-roam-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/brcmf-roam-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/compute-preempt-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/compute-preempt-tests.py", - "--out", - "{output}" - ], - "rust": "1.85.0" - }, - { - "name": "tools/asahi/compute-preempt-tests.py --old-ref f885ea0de2cd638bda1e0d3d38a901d927a503f5", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/compute-preempt-tests.py", - "--out", - "{output}", - "--old-ref", - "f885ea0de2cd638bda1e0d3d38a901d927a503f5" - ], - "rust": "1.85.0", - "expect": "pass", - "note": "self-checking; policy from c6f4ea502bdd^", - "commits": [ - "f885ea0de2cd638bda1e0d3d38a901d927a503f5" - ] - }, - { - "name": "tools/asahi/dcp14-backlight-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/dcp14-backlight-tests.py" - ] - }, - { - "name": "tools/asahi/dcp14-backlight-tests.py --old-source {output}/old.c", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/dcp14-backlight-tests.py", - "--old-source", - "{output}/old.c" - ], - "expect": "pass", - "note": "self-checking (-DOLD); iomfb_v14_7.c from 2e5a8e777ada^", - "setup": [ - [ - "sh", - "-c", - "mkdir -p \"$1\" && git show e19eb212f57ec32db9026666c89dd610062e9f24:drivers/gpu/drm/apple/iomfb_v14_7.c > \"$1/old.c\"", - "sh", - "{output}" - ] - ], - "commits": [ - "e19eb212f57ec32db9026666c89dd610062e9f24" - ] - }, - { - "name": "tools/asahi/dcp14-panel-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/dcp14-panel-tests.py" - ] - }, - { - "name": "tools/asahi/dcp14-panel-tests.py --old-source {output}/old.c", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/dcp14-panel-tests.py", - "--old-source", - "{output}/old.c" - ], - "expect": "pass", - "note": "self-checking; iomfb_v14_7.c from 7d5a612eceaa^", - "setup": [ - [ - "sh", - "-c", - "mkdir -p \"$1\" && git show a5713d126ea098fd3b6b833fc4022762540aac6c:drivers/gpu/drm/apple/iomfb_v14_7.c > \"$1/old.c\"", - "sh", - "{output}" - ] - ], - "commits": [ - "a5713d126ea098fd3b6b833fc4022762540aac6c" - ] - }, - { - "name": "tools/asahi/devcoredump-init-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/devcoredump-init-tests.py" - ] - }, - { - "name": "tools/asahi/devcoredump-init-tests.py --old-source {output}/old.c", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/devcoredump-init-tests.py", - "--old-source", - "{output}/old.c" - ], - "expect": "pass", - "note": "self-checking (-DOLD); devcoredump.c from ebd3dc459ac1^", - "setup": [ - [ - "sh", - "-c", - "mkdir -p \"$1\" && git show fb7cca304cb381dc589fadbed479c6f3b36e5e23:drivers/base/devcoredump.c > \"$1/old.c\"", - "sh", - "{output}" - ] - ], - "commits": [ - "fb7cca304cb381dc589fadbed479c6f3b36e5e23" - ] - }, - { - "name": "tools/asahi/display26-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/display26-tests.py" - ] - }, - { - "name": "tools/asahi/dma-heap-fd-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/dma-heap-fd-tests.py", - "--out", - "{output}", - "--old-ref", - "94380ecd5ff9b2a2b9f6e8a0b5c465159ccfaf71" - ], - "commits": [ - "94380ecd5ff9b2a2b9f6e8a0b5c465159ccfaf71" - ] - }, - { - "name": "tools/asahi/mt7932-association-events-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/mt7932-association-events-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/mt7932-association-events-tests.py --old-source {output}/old.c", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/mt7932-association-events-tests.py", - "--out", - "{output}", - "--old-source", - "{output}/old.c" - ], - "expect": "pass", - "note": "self-checking; link.c from c980f7435d50^ (the fix). The test commit itself (f885ea0de2cd) is test-only, so its parent is NOT a valid old source.", - "setup": [ - [ - "sh", - "-c", - "mkdir -p \"$1\" && git show 818bde483e1618783005ac76e1c58057a4bb4cfa:drivers/net/wireless/mediatek/mt7932/link.c > \"$1/old.c\"", - "sh", - "{output}" - ] - ], - "commits": [ - "818bde483e1618783005ac76e1c58057a4bb4cfa" - ] - }, - { - "name": "tools/asahi/mt7932-connect-result-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/mt7932-connect-result-tests.py", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/mt7932-connect-result-tests.py --old-source {output}/old.c", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/mt7932-connect-result-tests.py", - "--output", - "{output}", - "--old-source", - "{output}/old.c" - ], - "expect": "pass", - "note": "self-checking; link.c from 6c3ee18a0725^", - "setup": [ - [ - "sh", - "-c", - "mkdir -p \"$1\" && git show 7db4eadeeba6512726fc527cb624ae99742de607:drivers/net/wireless/mediatek/mt7932/link.c > \"$1/old.c\"", - "sh", - "{output}" - ] - ], - "commits": [ - "7db4eadeeba6512726fc527cb624ae99742de607" - ] - }, - { - "name": "tools/asahi/mt7932-owe-retry-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/mt7932-owe-retry-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/neo-auxiliary-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-auxiliary-tests.py", - "--output", - "{output}" - ], - "rust": "1.85.0" - }, - { - "name": "tools/asahi/neo-auxiliary-tests.py --mutation", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-auxiliary-tests.py", - "--output", - "{output}", - "--mutation" - ], - "rust": "1.85.0", - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/neo-auxiliary-tests.py --old", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-auxiliary-tests.py", - "--output", - "{output}", - "--old" - ], - "rust": "1.85.0", - "expect": "fail", - "note": "inverted; reads 990a45b609dd", - "commits": [ - "990a45b609ddcaa49f5386e1c11530599226c0e1" - ] - }, - { - "name": "tools/asahi/neo-stability-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-stability-tests.py", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/neo-stability-tests.py --mutation sep-syscore", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-stability-tests.py", - "--output", - "{output}", - "--mutation", - "sep-syscore" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/neo-stability-tests.py --mutation wake-irq", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-stability-tests.py", - "--output", - "{output}", - "--mutation", - "wake-irq" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/neo-stability-tests.py --mutation stale-policy", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-stability-tests.py", - "--output", - "{output}", - "--mutation", - "stale-policy" - ], - "expect": "fail", - "note": "inverted" - }, - { - "name": "tools/asahi/neo-stability-tests.py --old-usb", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-stability-tests.py", - "--output", - "{output}", - "--old-usb" - ], - "expect": "fail", - "note": "inverted; reads 990a45b609dd", - "commits": [ - "990a45b609ddcaa49f5386e1c11530599226c0e1" - ] - }, - { - "name": "tools/asahi/neo-stability-tests.py --old-regulatory", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/neo-stability-tests.py", - "--output", - "{output}", - "--old-regulatory" - ], - "expect": "fail", - "note": "inverted; reads 990a45b609dd", - "commits": [ - "990a45b609ddcaa49f5386e1c11530599226c0e1" - ] - }, - { - "name": "tools/asahi/pmp-j613-hwmon-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/pmp-j613-hwmon-tests.py" - ] - }, - { - "name": "tools/asahi/qspi-nvram-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/qspi-nvram-tests.py", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/rtkit-oslog-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/rtkit-oslog-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/rtkit-oslog-tests.py --mutate admission", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/rtkit-oslog-tests.py", - "--out", - "{output}", - "--mutate", - "admission" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutate)" - }, - { - "name": "tools/asahi/rtkit-oslog-tests.py --mutate channel", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/rtkit-oslog-tests.py", - "--out", - "{output}", - "--mutate", - "channel" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutate)" - }, - { - "name": "tools/asahi/rtkit-oslog-tests.py --mutate counter", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/rtkit-oslog-tests.py", - "--out", - "{output}", - "--mutate", - "counter" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutate)" - }, - { - "name": "tools/asahi/rtkit-oslog-tests.py --mutate reset", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/rtkit-oslog-tests.py", - "--out", - "{output}", - "--mutate", - "reset" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutate)" - }, - { - "name": "tools/asahi/rtkit-oslog-tests.py --ref 0cee491386e3dff5a09b60d5703f6a7be71b0883", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/rtkit-oslog-tests.py", - "--out", - "{output}", - "--ref", - "0cee491386e3dff5a09b60d5703f6a7be71b0883" - ], - "expect": "fail", - "note": "inverted; rtkit.c from 1369d18b4ec7^", - "commits": [ - "0cee491386e3dff5a09b60d5703f6a7be71b0883" - ] - }, - { - "name": "tools/asahi/tbnet-refill-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tbnet-refill-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/tbnet-refill-tests.py --mutant refill-count", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tbnet-refill-tests.py", - "--out", - "{output}", - "--mutant", - "refill-count" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutant)" - }, - { - "name": "tools/asahi/tbnet-refill-tests.py --mutant no-retry", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tbnet-refill-tests.py", - "--out", - "{output}", - "--mutant", - "no-retry" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutant)" - }, - { - "name": "tools/asahi/tbnet-refill-tests.py --mutant no-cancel", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tbnet-refill-tests.py", - "--out", - "{output}", - "--mutant", - "no-cancel" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutant)" - }, - { - "name": "tools/asahi/tbnet-refill-tests.py --mutant worker-allocation", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tbnet-refill-tests.py", - "--out", - "{output}", - "--mutant", - "worker-allocation" - ], - "expect": "fail", - "note": "inverted (note: flag is --mutant)" - }, - { - "name": "tools/asahi/tbnet-refill-tests.py --ref f7f5fc8e97cf5238c8669d9fca0d024b40174209", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tbnet-refill-tests.py", - "--out", - "{output}", - "--ref", - "f7f5fc8e97cf5238c8669d9fca0d024b40174209" - ], - "expect": "fail", - "note": "inverted; main.c from 71700a159448^", - "commits": [ - "f7f5fc8e97cf5238c8669d9fca0d024b40174209" - ] - }, - { - "name": "tools/asahi/tunnel-route-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tunnel-route-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/m3-batching-tests.rs", - "group": "tools", - "argv": [ - "{output}", - "--test-threads=1" - ], - "setup": [ - [ - "rustc", - "--edition=2021", - "--test", - "tools/asahi/m3-batching-tests.rs", - "-o", - "{output}" - ] - ], - "rust": "1.85.0" - }, - { - "name": "tools/asahi/m3-runtime-tests.rs", - "group": "tools", - "argv": [ - "{output}", - "--test-threads=1" - ], - "setup": [ - [ - "rustc", - "--edition=2021", - "--test", - "tools/asahi/m3-runtime-tests.rs", - "-o", - "{output}" - ] - ], - "rust": "1.85.0" - }, - { - "name": "tools/asahi/pmp-bootargs-tests.rs", - "group": "tools", - "argv": [ - "{output}", - "--test-threads=1" - ], - "setup": [ - [ - "rustc", - "--edition=2021", - "--test", - "tools/asahi/pmp-bootargs-tests.rs", - "-o", - "{output}" - ] - ], - "rust": "1.85.0" - }, - { - "name": "tools/asahi/pmp-power-tests.rs", - "group": "tools", - "argv": [ - "{output}", - "--test-threads=1" - ], - "setup": [ - [ - "rustc", - "--edition=2021", - "--test", - "tools/asahi/pmp-power-tests.rs", - "-o", - "{output}" - ] - ], - "rust": "1.85.0" - }, - { - "name": "tools/asahi-m3/test_completion_progress.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi-m3/test_completion_progress.py" - ] - }, - { - "name": "tools/asahi-m3/test_fault_snapshot.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi-m3/test_fault_snapshot.py" - ] - }, - { - "name": "tools/asahi-m3/test-retirement-progress.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi-m3/test-retirement-progress.py" - ] - }, - { - "name": "tools/asahi-m3/test-gem-object-size.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi-m3/test-gem-object-size.py" - ], - "rust": "1.85.0" - }, - { - "name": "tools/aurora-sep/tests/test_adt_extract.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_adt_extract.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_air_gpu.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_air_gpu.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_desktop_fixes.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_desktop_fixes.py", - "-v" - ], - "commits": [ - "05d6db3921dec6a6667a7ec8b1b0c75f0f66bafe" - ] - }, - { - "name": "tools/aurora-sep/tests/test_diag_sensor.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_diag_sensor.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_download_resume.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_download_resume.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_esp_history.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_esp_history.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_extract_mesa_calibration.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_extract_mesa_calibration.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_frozen_dependencies.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_frozen_dependencies.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_host_entropy.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_host_entropy.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_host_state_check.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_host_state_check.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m1n1_rollback.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m1n1_rollback.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_air.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_air.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_air_default.py", - "group": "aurora-sep-slow", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_air_default.py", - "-v" - ], - "commits": [ - "4f2a68f8b88a6b407f072356c248d22f076b401a" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_air_mesa.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_air_mesa.py", - "-v" - ], - "commits": [ - "4f2a68f8b88a6b407f072356c248d22f076b401a" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_boot_profile.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_boot_profile.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_cmdline.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_cmdline.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_flow.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_flow.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_gpu_check.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_gpu_check.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_handoff.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_handoff.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_kernel_only.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_kernel_only.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_max.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_max.py", - "-v" - ], - "commits": [ - "2ca6ff6e3480e11dfd51b720763c0fbd99fa015e", - "26cdc069a3dfa86b901ac3c407bad14cc2fb7efc" - ], - "serial": true - }, - { - "name": "tools/aurora-sep/tests/test_m3_persistent.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_persistent.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_pro_mesa.py", - "group": "aurora-sep-slow", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_pro_mesa.py", - "-v" - ], - "commits": [ - "26cdc069a3dfa86b901ac3c407bad14cc2fb7efc", - "df0c433027e9eb283b14a56894efcc817c8a3cfb", - "1d41e1b7ff5db2b31de8fcee56959d70ce152c0a" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_pro_perf.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_pro_perf.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_report.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_report.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3_stack_assembly.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3_stack_assembly.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_m3max_kit.py", - "group": "aurora-sep-slow", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_m3max_kit.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_neo_gpu.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_neo_gpu.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_packaging.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_packaging.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_pm_domains.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_pm_domains.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_ready_pam_hint.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_ready_pam_hint.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_refkey_seal_admission.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_refkey_seal_admission.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_release.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_release.py", - "-v" - ], - "commits": [ - "d14f756c9a65752ca532b93391f775387ba4f191", - "df0c433027e9eb283b14a56894efcc817c8a3cfb", - "1d41e1b7ff5db2b31de8fcee56959d70ce152c0a" - ] - }, - { - "name": "tools/aurora-sep/tests/test_sbio_exchange.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_sbio_exchange.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_sensor_recheck.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_sensor_recheck.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_sensor_wake.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_sensor_wake.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_sep_ready.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_sep_ready.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_store_shipped.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_store_shipped.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_t8112_sep_dma.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_t8112_sep_dma.py", - "-v" - ], - "setup": [ - [ - "make", - "-s", - "O={output}/kbuild", - "ARCH=arm64", - "allnoconfig" - ], - [ - "make", - "-s", - "O={output}/kbuild", - "ARCH=arm64", - "scripts_dtc" - ] - ], - "env": { - "DTC": "{output}/kbuild/scripts/dtc/dtc" - } - }, - { - "name": "tools/aurora-sep/tests/test_t8122_sep.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_t8122_sep.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_unsupported_soc.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_unsupported_soc.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_verify_bringup.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_verify_bringup.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/tests/test_xart_locator.py", - "group": "aurora-sep", - "argv": [ - "python3", - "-m", - "unittest", - "discover", - "-s", - "tools/aurora-sep/tests", - "-p", - "test_xart_locator.py", - "-v" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-udev-rule.sh", - "group": "tools", - "argv": [ - "sh", - "tools/aurora-sep/recipes/mesa-m3/tests/test-udev-rule.sh" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh (sh)", - "group": "tools", - "argv": [ - "sh", - "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh (bash)", - "group": "tools", - "argv": [ - "bash", - "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-user-setup.sh", - "group": "tools", - "argv": [ - "sh", - "tools/aurora-sep/recipes/mesa-m3/tests/test-user-setup.sh" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py", - "group": "tools", - "argv": [ - "python3", - "tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c", - "group": "tools", - "setup": [ - [ - "cc", - "-std=c11", - "-Wall", - "-Wextra", - "-Werror", - "-pedantic", - "-O2", - "tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c", - "-ldl", - "-o", - "{output}" - ] - ], - "argv": [ - "{output}" - ] - }, - { - "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-uwsm.sh", - "group": "tools", - "argv": [ - "sh", - "tools/aurora-sep/recipes/mesa-m3/tests/test-uwsm.sh" - ] - }, - { - "name": "tools/aurora-display-notifier/tests/test_notifier.py", - "group": "tools", - "argv": [ + "comment": [ + "Hermetic host test suites, run by host-tests.py from the top of the tree.", + "{output} is a fresh path that does not exist yet; its parent does.", + "expect: pass (exit 0, the default) or fail (non-zero; mutation checks that report a caught mutant that way).", + "known_failure: runs, is reported, and does not fail the job; remove it once the suite passes.", + "commits: history the suite reads; host-tests.py --fetch-commits fetches it into a shallow checkout.", + "Every test-like file under the discovery roots is listed in suites or excluded (tests/test_host_tests.py checks).", + "helpers: files that look like suites but are run by a listed suite.", + "serial: the suite measures time; it runs alone, after the parallel batch.", + "group: the CI job that runs the suite; aurora-sep-slow holds the longest aurora-sep suites so they run beside the rest." + ], + "apt": [ + "bison", + "bubblewrap", + "clang", + "dbus", + "device-tree-compiler", + "flex", + "libarchive-tools", + "libegl-dev", + "libgles-dev", + "libnotify-bin", + "lld", + "llvm", "make", - "-C", - "tools/aurora-display-notifier/", - "test" - ] - }, - { - "name": "tools/aurora-display-notifier/tests/integration_dbus.py", - "group": "tools", - "argv": [ - "dbus-run-session", - "--", - "env", - "AURORA_TEST_PRIVATE_BUS=1", - "python3", - "tools/aurora-display-notifier/tests/integration_dbus.py" - ] - }, - { - "name": "tools/aurora-tb/pr14-regression.py", - "group": "tools", - "argv": [ - "python3", - "tools/aurora-tb/pr14-regression.py", - "--negative-controls" - ] - }, - { - "name": "tools/aurora-tb/tbnet-lifecycle-test.py", - "group": "tools", - "argv": [ - "python3", - "tools/aurora-tb/tbnet-lifecycle-test.py", - "--source", - ".", - "--output", - "{output}", - "--negative-controls" - ], - "known_failure": "its fixture predates the receive refill retry (1d25feb6bb33): no rx_refill_work member or cancel_delayed_work_sync mock" - }, - { - "name": "tools/aurora-wake/test-s2idle-rtc-regression.sh", - "group": "tools", - "argv": [ - "bash", - "tools/aurora-wake/test-s2idle-rtc-regression.sh" - ] - }, - { - "name": "tools/aurora-userpages/userpages.c", - "group": "tools", - "argv": [ - "cc", - "-O2", - "-g", - "-Wall", - "-Wextra", - "-Werror", - "-Wl,-z,max-page-size=65536", - "tools/aurora-userpages/userpages.c", - "-o", - "{output}" - ] - }, - { - "name": "tools/testing/apple/check-backlight.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/apple/check-backlight.py", - "--output", - "{output}" - ], - "known_failure": "its include path lacks linux/math64.h, which dcp_backlight.h now includes" - }, - { - "name": "tools/testing/apple/check-backlight-retry.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/apple/check-backlight-retry.py", - "--output", - "{output}" - ], - "known_failure": "the extracted code now calls min(), div_u64() and mul_u32_u32(), which the fixture does not mock" - }, - { - "name": "tools/testing/apple/check-lifetime.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/apple/check-lifetime.py", - "--output", - "{output}" - ], - "known_failure": "the apple_dcp_afkep mock lacks lock and stopping, and spin_lock_irqsave is not mocked" - }, - { - "name": "tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh", - "group": "tools", - "argv": [ - "sh", - "tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh" - ], - "known_failure": "the send and edid stages predate afk.c changes (afk_send_command_common, reply_len)" - }, - { - "name": "tools/testing/selftests/drivers/gpu/apple-afkv2/test-admission.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/selftests/drivers/gpu/apple-afkv2/test-admission.py" - ] - }, - { - "name": "tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py", - "--mutations", - "--output", - "{output}" - ] - }, - { - "name": "tools/testing/selftests/drivers/gpu/apple-dcp/run.sh", - "group": "tools", - "argv": [ - "sh", - "tools/testing/selftests/drivers/gpu/apple-dcp/run.sh", - "--output", - "{output}" - ] - }, - { - "name": "tools/testing/selftests/drivers/gpu/test_t8140_external_bw.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/selftests/drivers/gpu/test_t8140_external_bw.py", - "-v" - ] - }, - { - "name": "tools/testing/selftests/drivers/net/mt7932_e2e_test.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/selftests/drivers/net/mt7932_e2e_test.py", - "-v" - ] - }, - { - "name": "tools/asahi/tb-dp-repair-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tb-dp-repair-tests.py", - "--mutations", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/t6030-clock-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/t6030-clock-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/t6030-wiring-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/t6030-wiring-tests.py", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/tiled-split-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/tiled-split-tests.py", - "--mutations", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/t6030-pmp-supplier-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/t6030-pmp-supplier-tests.py", - "--mutations", - "--output", - "{output}" - ] - }, - { - "name": "tools/asahi/atc-usb3-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/atc-usb3-tests.py", - "--mutations", - "--out", - "{output}" - ] - }, - { - "name": "tools/asahi/g15-input-error-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-input-error-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-queue-credit-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-queue-credit-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-probe-init-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-probe-init-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-retention-tests.py", - "group": "tools", - "commits": [ - "31da169251bd34d5810a0874563d9a019b85bf56" - ], - "argv": [ - "python3", - "tools/asahi/g15-retention-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual G15 and KBox ownership bodies with drop-count allocation failures; old-source P1 controls must fail at runtime. Public synthetic inputs only." - }, - { - "name": "tools/asahi/g15-maintained-handoff-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-maintained-handoff-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-zero-words-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-zero-words-tests.py", - "--mutations", - "--evidence", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-pb-growth-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-pb-growth-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-stats-drain-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-stats-drain-tests.py", - "--mutations", - "--output", - "{output}" - ], - "rust": "1.93.1", - "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." - }, - { - "name": "tools/asahi/g15-display-order-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/g15-display-order-tests.py" - ], - "rust": "1.93.1", - "note": "Actual display publication, T6030/T6031 link and admit() bodies on OF/device-link and admission-order models; C and admit() mutants must build and fail. No hardware or private fixtures." - }, - { - "name": "tools/testing/selftests/drivers/gpu/asahi-g15/run.py", - "group": "tools", - "argv": [ - "python3", - "tools/testing/selftests/drivers/gpu/asahi-g15/run.py" - ], - "rust": "1.93.1", - "note": "Public synthetic Rust/C host cases; two unavailable private-hardware fixture cases remain explicitly ignored." - }, - { - "name": "tools/asahi/t6030-pmp-dt-tests.py", - "group": "tools", - "setup": [ - [ - "make", - "-s", - "O={output}/kbuild", - "ARCH=arm64", - "allnoconfig" - ], - [ - "make", - "-s", - "O={output}/kbuild", - "ARCH=arm64", - "scripts_dtc" - ] - ], - "argv": [ - "python3", - "tools/asahi/t6030-pmp-dt-tests.py", - "--dtc", - "{output}/kbuild/scripts/dtc/dtc", - "--output", - "{output}/dtbs" - ], - "note": "Compile J514S/J516S default and static PMP profiles; check supplier nodes and alias in the resulting DTBs." - }, - { - "name": "tools/asahi/t6030-pmp-graph-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/t6030-pmp-graph-tests.py", - "--mutations", - "--output", - "{output}" - ], - "note": "Actual production-body host boundaries and runtime mutants; no firmware or hardware qualification." - }, - { - "name": "tools/asahi/t6030-display-admission-tests.py", - "group": "tools", - "argv": [ - "python3", - "tools/asahi/t6030-display-admission-tests.py", - "--mutations", - "--output", - "{output}" - ], - "note": "Actual production-body host boundaries and runtime mutants; no firmware or hardware qualification." - }, - { - "name": "tools/asahi/t6030-display-linkage-tests.py", - "group": "tools", - "setup": [ - [ - "make", - "-s", - "O={output}-conf", - "allnoconfig" - ] - ], - "argv": [ - "python3", - "tools/asahi/t6030-display-linkage-tests.py", - "--conf", - "{output}-conf/scripts/kconfig/conf", - "--output", - "{output}" - ], - "note": "Build only the host Kconfig tool in an isolated output directory; check all built-in/module dependency combinations." + "makepkg", + "python3-gi", + "zstd" + ], + "discovery": [ + "tools/asahi", + "tools/asahi-m3", + "tools/aurora-*", + "tools/testing/apple", + "tools/testing/selftests/drivers/apple", + "tools/testing/selftests/drivers/gpu/apple-*", + "tools/testing/selftests/drivers/gpu/asahi-g15" + ], + "suites": [ + { + "name": "tools/asahi/apple-dart-fw-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/apple-dart-fw-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/apple-dart-range-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/apple-dart-range-tests.py" + ], + "known_failure": "its mocks predate the current apple-dart.c (no u32, fw_mirror/fw_handoff, apple_dart_fw_invalidate; apple_dart_hw_sync_locked takes three arguments now)" + }, + { + "name": "tools/asahi/avd-codec-tail-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/avd-codec-tail-tests.py", + "--mutations", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/avd-av1-tail-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/avd-av1-tail-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/avd-av1-tail-tests.py --mutation round-up", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/avd-av1-tail-tests.py", + "--out", + "{output}", + "--mutation", + "round-up" + ], + "expect": "fail", + "note": "inverted: exit status is the controls program status, non-zero when the mutant is caught" + }, + { + "name": "tools/asahi/avd-av1-tail-tests.py --mutation cpu-unaligned", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/avd-av1-tail-tests.py", + "--out", + "{output}", + "--mutation", + "cpu-unaligned" + ], + "expect": "fail", + "note": "inverted: exit status is the controls program status, non-zero when the mutant is caught" + }, + { + "name": "tools/asahi/bcm4377-id-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-id-tests.py", + "--out", + "{output}", + "--old-ref", + "87ddae1397f6f70817cb57df76477a86bdf96424" + ], + "commits": [ + "87ddae1397f6f70817cb57df76477a86bdf96424" + ] + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py --mutation epoch", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}", + "--mutation", + "epoch" + ], + "expect": "pass", + "note": "self-checking: exits 0 when the mutant aborts" + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py --mutation close", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}", + "--mutation", + "close" + ], + "expect": "pass", + "note": "self-checking: exits 0 when the mutant aborts" + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py --mutation producer-fence", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}", + "--mutation", + "producer-fence" + ], + "expect": "pass", + "note": "self-checking: exits 0 when the mutant aborts" + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py --mutation active-fence", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}", + "--mutation", + "active-fence" + ], + "expect": "pass", + "note": "self-checking: exits 0 when the mutant aborts" + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py --mutation board", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}", + "--mutation", + "board" + ], + "expect": "pass", + "note": "self-checking: exits 0 when the mutant aborts" + }, + { + "name": "tools/asahi/bcm4377-resume-tests.py --old-ref 36fab84538bed0f2f823c0e6745e4b0f5e6c2a1e", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/bcm4377-resume-tests.py", + "--out", + "{output}", + "--old-ref", + "36fab84538bed0f2f823c0e6745e4b0f5e6c2a1e" + ], + "expect": "pass", + "note": "self-checking: original driver (87ddae1397f6^) must abort cases 0,1,2,28", + "commits": [ + "36fab84538bed0f2f823c0e6745e4b0f5e6c2a1e" + ] + }, + { + "name": "tools/asahi/brcmf-assoc-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-assoc-tests.py", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/brcmf-assoc-tests.py --mutation short-ie", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-assoc-tests.py", + "--output", + "{output}", + "--mutation", + "short-ie" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-assoc-tests.py --mutation header-length", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-assoc-tests.py", + "--output", + "{output}", + "--mutation", + "header-length" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-assoc-tests.py --mutation skb-capacity", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-assoc-tests.py", + "--output", + "{output}", + "--mutation", + "skb-capacity" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-assoc-tests.py --mutation cmplt-header", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-assoc-tests.py", + "--output", + "{output}", + "--mutation", + "cmplt-header" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-assoc-tests.py --old-association f58ae50192550e6fc67c956f361f525881a1f969", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-assoc-tests.py", + "--output", + "{output}", + "--old-association", + "f58ae50192550e6fc67c956f361f525881a1f969" + ], + "expect": "fail", + "note": "inverted; old getter from ecd1ea4d0f06^", + "commits": [ + "f58ae50192550e6fc67c956f361f525881a1f969" + ] + }, + { + "name": "tools/asahi/brcmf-bss-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-bss-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/brcmfmac-scan-timeout-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmfmac-scan-timeout-tests.py", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/brcmfmac-scan-timeout-tests.py --old-source {output}/old.c", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmfmac-scan-timeout-tests.py", + "--output", + "{output}", + "--old-source", + "{output}/old.c" + ], + "expect": "pass", + "note": "self-checking; cfg80211.c from f58ae5019255^", + "setup": [ + [ + "sh", + "-c", + "mkdir -p \"$1\" && git show 661c0a5740e8f36816f7e79a784c36c026097368:drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c > \"$1/old.c\"", + "sh", + "{output}" + ] + ], + "commits": [ + "661c0a5740e8f36816f7e79a784c36c026097368" + ] + }, + { + "name": "tools/asahi/brcmf-pcie-dma-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-pcie-dma-tests.py", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/brcmf-pcie-dma-tests.py --mutation narrow-stream", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-pcie-dma-tests.py", + "--output", + "{output}", + "--mutation", + "narrow-stream" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-pcie-dma-tests.py --mutation ignore-aperture", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-pcie-dma-tests.py", + "--output", + "{output}", + "--mutation", + "ignore-aperture" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-pcie-dma-tests.py --mutation revision-wide", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-pcie-dma-tests.py", + "--output", + "{output}", + "--mutation", + "revision-wide" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/brcmf-pcie-dma-tests.py --old-dma 654386a164a1e93063f879491f10bebfc1439291", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-pcie-dma-tests.py", + "--output", + "{output}", + "--old-dma", + "654386a164a1e93063f879491f10bebfc1439291" + ], + "expect": "fail", + "note": "inverted; probe from dbdeefb63947^", + "commits": [ + "654386a164a1e93063f879491f10bebfc1439291" + ] + }, + { + "name": "tools/asahi/brcmf-pcie-dma-tests.py --firmware-revision b0de24a8c59735f21023c93592dd9a2eb9e155a4", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-pcie-dma-tests.py", + "--output", + "{output}", + "--firmware-revision", + "b0de24a8c59735f21023c93592dd9a2eb9e155a4" + ], + "expect": "fail", + "note": "inverted; firmware table from 2d17f269503c^", + "commits": [ + "b0de24a8c59735f21023c93592dd9a2eb9e155a4" + ] + }, + { + "name": "tools/asahi/brcmf-roam-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/brcmf-roam-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/compute-preempt-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/compute-preempt-tests.py", + "--out", + "{output}" + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/compute-preempt-tests.py --old-ref f885ea0de2cd638bda1e0d3d38a901d927a503f5", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/compute-preempt-tests.py", + "--out", + "{output}", + "--old-ref", + "f885ea0de2cd638bda1e0d3d38a901d927a503f5" + ], + "rust": "1.85.0", + "expect": "pass", + "note": "self-checking; policy from c6f4ea502bdd^", + "commits": [ + "f885ea0de2cd638bda1e0d3d38a901d927a503f5" + ] + }, + { + "name": "tools/asahi/dcp14-backlight-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcp14-backlight-tests.py" + ] + }, + { + "name": "tools/asahi/dcp14-backlight-tests.py --old-source {output}/old.c", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcp14-backlight-tests.py", + "--old-source", + "{output}/old.c" + ], + "expect": "pass", + "note": "self-checking (-DOLD); iomfb_v14_7.c from 2e5a8e777ada^", + "setup": [ + [ + "sh", + "-c", + "mkdir -p \"$1\" && git show e19eb212f57ec32db9026666c89dd610062e9f24:drivers/gpu/drm/apple/iomfb_v14_7.c > \"$1/old.c\"", + "sh", + "{output}" + ] + ], + "commits": [ + "e19eb212f57ec32db9026666c89dd610062e9f24" + ] + }, + { + "name": "tools/asahi/dcp14-panel-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcp14-panel-tests.py" + ] + }, + { + "name": "tools/asahi/dcp14-panel-tests.py --old-source {output}/old.c", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcp14-panel-tests.py", + "--old-source", + "{output}/old.c" + ], + "expect": "pass", + "note": "self-checking; iomfb_v14_7.c from 7d5a612eceaa^", + "setup": [ + [ + "sh", + "-c", + "mkdir -p \"$1\" && git show a5713d126ea098fd3b6b833fc4022762540aac6c:drivers/gpu/drm/apple/iomfb_v14_7.c > \"$1/old.c\"", + "sh", + "{output}" + ] + ], + "commits": [ + "a5713d126ea098fd3b6b833fc4022762540aac6c" + ] + }, + { + "name": "tools/asahi/devcoredump-init-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/devcoredump-init-tests.py" + ] + }, + { + "name": "tools/asahi/devcoredump-init-tests.py --old-source {output}/old.c", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/devcoredump-init-tests.py", + "--old-source", + "{output}/old.c" + ], + "expect": "pass", + "note": "self-checking (-DOLD); devcoredump.c from ebd3dc459ac1^", + "setup": [ + [ + "sh", + "-c", + "mkdir -p \"$1\" && git show fb7cca304cb381dc589fadbed479c6f3b36e5e23:drivers/base/devcoredump.c > \"$1/old.c\"", + "sh", + "{output}" + ] + ], + "commits": [ + "fb7cca304cb381dc589fadbed479c6f3b36e5e23" + ] + }, + { + "name": "tools/asahi/display26-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/display26-tests.py" + ] + }, + { + "name": "tools/asahi/dma-heap-fd-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dma-heap-fd-tests.py", + "--out", + "{output}", + "--old-ref", + "94380ecd5ff9b2a2b9f6e8a0b5c465159ccfaf71" + ], + "commits": [ + "94380ecd5ff9b2a2b9f6e8a0b5c465159ccfaf71" + ] + }, + { + "name": "tools/asahi/mt7932-association-events-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/mt7932-association-events-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/mt7932-association-events-tests.py --old-source {output}/old.c", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/mt7932-association-events-tests.py", + "--out", + "{output}", + "--old-source", + "{output}/old.c" + ], + "expect": "pass", + "note": "self-checking; link.c from c980f7435d50^ (the fix). The test commit itself (f885ea0de2cd) is test-only, so its parent is NOT a valid old source.", + "setup": [ + [ + "sh", + "-c", + "mkdir -p \"$1\" && git show 818bde483e1618783005ac76e1c58057a4bb4cfa:drivers/net/wireless/mediatek/mt7932/link.c > \"$1/old.c\"", + "sh", + "{output}" + ] + ], + "commits": [ + "818bde483e1618783005ac76e1c58057a4bb4cfa" + ] + }, + { + "name": "tools/asahi/mt7932-connect-result-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/mt7932-connect-result-tests.py", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/mt7932-connect-result-tests.py --old-source {output}/old.c", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/mt7932-connect-result-tests.py", + "--output", + "{output}", + "--old-source", + "{output}/old.c" + ], + "expect": "pass", + "note": "self-checking; link.c from 6c3ee18a0725^", + "setup": [ + [ + "sh", + "-c", + "mkdir -p \"$1\" && git show 7db4eadeeba6512726fc527cb624ae99742de607:drivers/net/wireless/mediatek/mt7932/link.c > \"$1/old.c\"", + "sh", + "{output}" + ] + ], + "commits": [ + "7db4eadeeba6512726fc527cb624ae99742de607" + ] + }, + { + "name": "tools/asahi/mt7932-owe-retry-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/mt7932-owe-retry-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/neo-auxiliary-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-auxiliary-tests.py", + "--output", + "{output}" + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/neo-auxiliary-tests.py --mutation", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-auxiliary-tests.py", + "--output", + "{output}", + "--mutation" + ], + "rust": "1.85.0", + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/neo-auxiliary-tests.py --old", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-auxiliary-tests.py", + "--output", + "{output}", + "--old" + ], + "rust": "1.85.0", + "expect": "fail", + "note": "inverted; reads 990a45b609dd", + "commits": [ + "990a45b609ddcaa49f5386e1c11530599226c0e1" + ] + }, + { + "name": "tools/asahi/neo-stability-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-stability-tests.py", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/neo-stability-tests.py --mutation sep-syscore", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-stability-tests.py", + "--output", + "{output}", + "--mutation", + "sep-syscore" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/neo-stability-tests.py --mutation wake-irq", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-stability-tests.py", + "--output", + "{output}", + "--mutation", + "wake-irq" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/neo-stability-tests.py --mutation stale-policy", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-stability-tests.py", + "--output", + "{output}", + "--mutation", + "stale-policy" + ], + "expect": "fail", + "note": "inverted" + }, + { + "name": "tools/asahi/neo-stability-tests.py --old-usb", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-stability-tests.py", + "--output", + "{output}", + "--old-usb" + ], + "expect": "fail", + "note": "inverted; reads 990a45b609dd", + "commits": [ + "990a45b609ddcaa49f5386e1c11530599226c0e1" + ] + }, + { + "name": "tools/asahi/neo-stability-tests.py --old-regulatory", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/neo-stability-tests.py", + "--output", + "{output}", + "--old-regulatory" + ], + "expect": "fail", + "note": "inverted; reads 990a45b609dd", + "commits": [ + "990a45b609ddcaa49f5386e1c11530599226c0e1" + ] + }, + { + "name": "tools/asahi/pmp-j613-hwmon-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/pmp-j613-hwmon-tests.py" + ] + }, + { + "name": "tools/asahi/qspi-nvram-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/qspi-nvram-tests.py", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/rtkit-oslog-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/rtkit-oslog-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/rtkit-oslog-tests.py --mutate admission", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/rtkit-oslog-tests.py", + "--out", + "{output}", + "--mutate", + "admission" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutate)" + }, + { + "name": "tools/asahi/rtkit-oslog-tests.py --mutate channel", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/rtkit-oslog-tests.py", + "--out", + "{output}", + "--mutate", + "channel" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutate)" + }, + { + "name": "tools/asahi/rtkit-oslog-tests.py --mutate counter", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/rtkit-oslog-tests.py", + "--out", + "{output}", + "--mutate", + "counter" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutate)" + }, + { + "name": "tools/asahi/rtkit-oslog-tests.py --mutate reset", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/rtkit-oslog-tests.py", + "--out", + "{output}", + "--mutate", + "reset" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutate)" + }, + { + "name": "tools/asahi/rtkit-oslog-tests.py --ref 0cee491386e3dff5a09b60d5703f6a7be71b0883", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/rtkit-oslog-tests.py", + "--out", + "{output}", + "--ref", + "0cee491386e3dff5a09b60d5703f6a7be71b0883" + ], + "expect": "fail", + "note": "inverted; rtkit.c from 1369d18b4ec7^", + "commits": [ + "0cee491386e3dff5a09b60d5703f6a7be71b0883" + ] + }, + { + "name": "tools/asahi/tbnet-refill-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tbnet-refill-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/tbnet-refill-tests.py --mutant refill-count", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tbnet-refill-tests.py", + "--out", + "{output}", + "--mutant", + "refill-count" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutant)" + }, + { + "name": "tools/asahi/tbnet-refill-tests.py --mutant no-retry", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tbnet-refill-tests.py", + "--out", + "{output}", + "--mutant", + "no-retry" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutant)" + }, + { + "name": "tools/asahi/tbnet-refill-tests.py --mutant no-cancel", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tbnet-refill-tests.py", + "--out", + "{output}", + "--mutant", + "no-cancel" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutant)" + }, + { + "name": "tools/asahi/tbnet-refill-tests.py --mutant worker-allocation", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tbnet-refill-tests.py", + "--out", + "{output}", + "--mutant", + "worker-allocation" + ], + "expect": "fail", + "note": "inverted (note: flag is --mutant)" + }, + { + "name": "tools/asahi/tbnet-refill-tests.py --ref f7f5fc8e97cf5238c8669d9fca0d024b40174209", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tbnet-refill-tests.py", + "--out", + "{output}", + "--ref", + "f7f5fc8e97cf5238c8669d9fca0d024b40174209" + ], + "expect": "fail", + "note": "inverted; main.c from 71700a159448^", + "commits": [ + "f7f5fc8e97cf5238c8669d9fca0d024b40174209" + ] + }, + { + "name": "tools/asahi/tunnel-route-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tunnel-route-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/m3-batching-tests.rs", + "group": "tools", + "argv": [ + "{output}", + "--test-threads=1" + ], + "setup": [ + [ + "rustc", + "--edition=2021", + "--test", + "tools/asahi/m3-batching-tests.rs", + "-o", + "{output}" + ] + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/m3-runtime-tests.rs", + "group": "tools", + "argv": [ + "{output}", + "--test-threads=1" + ], + "setup": [ + [ + "rustc", + "--edition=2021", + "--test", + "tools/asahi/m3-runtime-tests.rs", + "-o", + "{output}" + ] + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/pmp-bootargs-tests.rs", + "group": "tools", + "argv": [ + "{output}", + "--test-threads=1" + ], + "setup": [ + [ + "rustc", + "--edition=2021", + "--test", + "tools/asahi/pmp-bootargs-tests.rs", + "-o", + "{output}" + ] + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/pmp-power-tests.rs", + "group": "tools", + "argv": [ + "{output}", + "--test-threads=1" + ], + "setup": [ + [ + "rustc", + "--edition=2021", + "--test", + "tools/asahi/pmp-power-tests.rs", + "-o", + "{output}" + ] + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi-m3/test_completion_progress.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi-m3/test_completion_progress.py" + ] + }, + { + "name": "tools/asahi-m3/test_fault_snapshot.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi-m3/test_fault_snapshot.py" + ] + }, + { + "name": "tools/asahi-m3/test-retirement-progress.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi-m3/test-retirement-progress.py" + ] + }, + { + "name": "tools/asahi-m3/test-gem-object-size.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi-m3/test-gem-object-size.py" + ], + "rust": "1.85.0" + }, + { + "name": "tools/aurora-sep/tests/test_adt_extract.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_adt_extract.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_air_gpu.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_air_gpu.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_desktop_fixes.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_desktop_fixes.py", + "-v" + ], + "commits": [ + "05d6db3921dec6a6667a7ec8b1b0c75f0f66bafe" + ] + }, + { + "name": "tools/aurora-sep/tests/test_diag_sensor.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_diag_sensor.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_download_resume.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_download_resume.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_esp_history.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_esp_history.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_extract_mesa_calibration.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_extract_mesa_calibration.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_frozen_dependencies.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_frozen_dependencies.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_host_entropy.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_host_entropy.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_host_state_check.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_host_state_check.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m1n1_rollback.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m1n1_rollback.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_air.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_air.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_air_default.py", + "group": "aurora-sep-slow", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_air_default.py", + "-v" + ], + "commits": [ + "4f2a68f8b88a6b407f072356c248d22f076b401a" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_air_mesa.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_air_mesa.py", + "-v" + ], + "commits": [ + "4f2a68f8b88a6b407f072356c248d22f076b401a" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_boot_profile.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_boot_profile.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_cmdline.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_cmdline.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_flow.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_flow.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_gpu_check.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_gpu_check.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_handoff.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_handoff.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_kernel_only.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_kernel_only.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_max.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_max.py", + "-v" + ], + "commits": [ + "2ca6ff6e3480e11dfd51b720763c0fbd99fa015e", + "26cdc069a3dfa86b901ac3c407bad14cc2fb7efc" + ], + "serial": true + }, + { + "name": "tools/aurora-sep/tests/test_m3_persistent.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_persistent.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_pro_mesa.py", + "group": "aurora-sep-slow", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_pro_mesa.py", + "-v" + ], + "commits": [ + "26cdc069a3dfa86b901ac3c407bad14cc2fb7efc", + "df0c433027e9eb283b14a56894efcc817c8a3cfb", + "1d41e1b7ff5db2b31de8fcee56959d70ce152c0a" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_pro_perf.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_pro_perf.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_report.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_report.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3_stack_assembly.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3_stack_assembly.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_m3max_kit.py", + "group": "aurora-sep-slow", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_m3max_kit.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_neo_gpu.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_neo_gpu.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_packaging.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_packaging.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_pm_domains.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_pm_domains.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_ready_pam_hint.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_ready_pam_hint.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_refkey_seal_admission.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_refkey_seal_admission.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_release.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_release.py", + "-v" + ], + "commits": [ + "d14f756c9a65752ca532b93391f775387ba4f191", + "df0c433027e9eb283b14a56894efcc817c8a3cfb", + "1d41e1b7ff5db2b31de8fcee56959d70ce152c0a" + ] + }, + { + "name": "tools/aurora-sep/tests/test_sbio_exchange.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_sbio_exchange.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_sensor_recheck.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_sensor_recheck.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_sensor_wake.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_sensor_wake.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_sep_ready.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_sep_ready.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_store_shipped.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_store_shipped.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_t8112_sep_dma.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_t8112_sep_dma.py", + "-v" + ], + "setup": [ + [ + "make", + "-s", + "O={output}/kbuild", + "ARCH=arm64", + "allnoconfig" + ], + [ + "make", + "-s", + "O={output}/kbuild", + "ARCH=arm64", + "scripts_dtc" + ] + ], + "env": { + "DTC": "{output}/kbuild/scripts/dtc/dtc" + } + }, + { + "name": "tools/aurora-sep/tests/test_t8122_sep.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_t8122_sep.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_unsupported_soc.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_unsupported_soc.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_verify_bringup.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_verify_bringup.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/tests/test_xart_locator.py", + "group": "aurora-sep", + "argv": [ + "python3", + "-m", + "unittest", + "discover", + "-s", + "tools/aurora-sep/tests", + "-p", + "test_xart_locator.py", + "-v" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-udev-rule.sh", + "group": "tools", + "argv": [ + "sh", + "tools/aurora-sep/recipes/mesa-m3/tests/test-udev-rule.sh" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh (sh)", + "group": "tools", + "argv": [ + "sh", + "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh (bash)", + "group": "tools", + "argv": [ + "bash", + "tools/aurora-sep/recipes/mesa-m3/tests/test-session-env.sh" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-user-setup.sh", + "group": "tools", + "argv": [ + "sh", + "tools/aurora-sep/recipes/mesa-m3/tests/test-user-setup.sh" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py", + "group": "tools", + "argv": [ + "python3", + "tools/aurora-sep/recipes/mesa-m3/tests/test-system-provider.py" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c", + "group": "tools", + "setup": [ + [ + "cc", + "-std=c11", + "-Wall", + "-Wextra", + "-Werror", + "-pedantic", + "-O2", + "tools/aurora-sep/recipes/mesa-m3/tests/test-system-board.c", + "-ldl", + "-o", + "{output}" + ] + ], + "argv": [ + "{output}" + ] + }, + { + "name": "tools/aurora-sep/recipes/mesa-m3/tests/test-uwsm.sh", + "group": "tools", + "argv": [ + "sh", + "tools/aurora-sep/recipes/mesa-m3/tests/test-uwsm.sh" + ] + }, + { + "name": "tools/aurora-display-notifier/tests/test_notifier.py", + "group": "tools", + "argv": [ + "make", + "-C", + "tools/aurora-display-notifier/", + "test" + ] + }, + { + "name": "tools/aurora-display-notifier/tests/integration_dbus.py", + "group": "tools", + "argv": [ + "dbus-run-session", + "--", + "env", + "AURORA_TEST_PRIVATE_BUS=1", + "python3", + "tools/aurora-display-notifier/tests/integration_dbus.py" + ] + }, + { + "name": "tools/aurora-tb/pr14-regression.py", + "group": "tools", + "argv": [ + "python3", + "tools/aurora-tb/pr14-regression.py", + "--negative-controls" + ] + }, + { + "name": "tools/aurora-tb/tbnet-lifecycle-test.py", + "group": "tools", + "argv": [ + "python3", + "tools/aurora-tb/tbnet-lifecycle-test.py", + "--source", + ".", + "--output", + "{output}", + "--negative-controls" + ], + "known_failure": "its fixture predates the receive refill retry (1d25feb6bb33): no rx_refill_work member or cancel_delayed_work_sync mock" + }, + { + "name": "tools/aurora-wake/test-s2idle-rtc-regression.sh", + "group": "tools", + "argv": [ + "bash", + "tools/aurora-wake/test-s2idle-rtc-regression.sh" + ] + }, + { + "name": "tools/aurora-userpages/userpages.c", + "group": "tools", + "argv": [ + "cc", + "-O2", + "-g", + "-Wall", + "-Wextra", + "-Werror", + "-Wl,-z,max-page-size=65536", + "tools/aurora-userpages/userpages.c", + "-o", + "{output}" + ] + }, + { + "name": "tools/testing/apple/check-backlight.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/apple/check-backlight.py", + "--output", + "{output}" + ], + "known_failure": "its include path lacks linux/math64.h, which dcp_backlight.h now includes" + }, + { + "name": "tools/testing/apple/check-backlight-retry.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/apple/check-backlight-retry.py", + "--output", + "{output}" + ], + "known_failure": "the extracted code now calls min(), div_u64() and mul_u32_u32(), which the fixture does not mock" + }, + { + "name": "tools/testing/apple/check-lifetime.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/apple/check-lifetime.py", + "--output", + "{output}" + ], + "known_failure": "the apple_dcp_afkep mock lacks lock and stopping, and spin_lock_irqsave is not mocked" + }, + { + "name": "tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh", + "group": "tools", + "argv": [ + "sh", + "tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh" + ], + "known_failure": "the send and edid stages predate afk.c changes (afk_send_command_common, reply_len)" + }, + { + "name": "tools/testing/selftests/drivers/gpu/apple-afkv2/test-admission.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/selftests/drivers/gpu/apple-afkv2/test-admission.py" + ] + }, + { + "name": "tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py", + "--mutations", + "--output", + "{output}" + ] + }, + { + "name": "tools/testing/selftests/drivers/gpu/apple-dcp/run.sh", + "group": "tools", + "argv": [ + "sh", + "tools/testing/selftests/drivers/gpu/apple-dcp/run.sh", + "--output", + "{output}" + ] + }, + { + "name": "tools/testing/selftests/drivers/gpu/test_t8140_external_bw.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/selftests/drivers/gpu/test_t8140_external_bw.py", + "-v" + ] + }, + { + "name": "tools/testing/selftests/drivers/net/mt7932_e2e_test.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/selftests/drivers/net/mt7932_e2e_test.py", + "-v" + ] + }, + { + "name": "tools/asahi/tb-dp-repair-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tb-dp-repair-tests.py", + "--mutations", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/t6030-clock-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/t6030-clock-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/t6030-wiring-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/t6030-wiring-tests.py", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/tiled-split-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/tiled-split-tests.py", + "--mutations", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/t6030-pmp-supplier-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/t6030-pmp-supplier-tests.py", + "--mutations", + "--output", + "{output}" + ] + }, + { + "name": "tools/asahi/atc-usb3-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/atc-usb3-tests.py", + "--mutations", + "--out", + "{output}" + ] + }, + { + "name": "tools/asahi/g15-input-error-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-input-error-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-queue-credit-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-queue-credit-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-probe-init-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-probe-init-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-retention-tests.py", + "group": "tools", + "commits": [ + "31da169251bd34d5810a0874563d9a019b85bf56" + ], + "argv": [ + "python3", + "tools/asahi/g15-retention-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual G15 and KBox ownership bodies with drop-count allocation failures; old-source P1 controls must fail at runtime. Public synthetic inputs only." + }, + { + "name": "tools/asahi/g15-maintained-handoff-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-maintained-handoff-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-zero-words-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-zero-words-tests.py", + "--mutations", + "--evidence", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-pb-growth-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-pb-growth-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-stats-drain-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-stats-drain-tests.py", + "--mutations", + "--output", + "{output}" + ], + "rust": "1.93.1", + "note": "Actual production-body host control plus runtime mutants; kernel services modeled, no private hardware fixtures." + }, + { + "name": "tools/asahi/g15-display-order-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/g15-display-order-tests.py" + ], + "rust": "1.93.1", + "note": "Actual display publication, T6030/T6031 link and admit() bodies on OF/device-link and admission-order models; C and admit() mutants must build and fail. No hardware or private fixtures." + }, + { + "name": "tools/testing/selftests/drivers/gpu/asahi-g15/run.py", + "group": "tools", + "argv": [ + "python3", + "tools/testing/selftests/drivers/gpu/asahi-g15/run.py" + ], + "rust": "1.93.1", + "note": "Public synthetic Rust/C host cases; two unavailable private-hardware fixture cases remain explicitly ignored." + }, + { + "name": "tools/asahi/t6030-pmp-dt-tests.py", + "group": "tools", + "setup": [ + [ + "make", + "-s", + "O={output}/kbuild", + "ARCH=arm64", + "allnoconfig" + ], + [ + "make", + "-s", + "O={output}/kbuild", + "ARCH=arm64", + "scripts_dtc" + ] + ], + "argv": [ + "python3", + "tools/asahi/t6030-pmp-dt-tests.py", + "--dtc", + "{output}/kbuild/scripts/dtc/dtc", + "--output", + "{output}/dtbs" + ], + "note": "Compile J514S/J516S default and static PMP profiles; check supplier nodes and alias in the resulting DTBs." + }, + { + "name": "tools/asahi/t6030-pmp-graph-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/t6030-pmp-graph-tests.py", + "--mutations", + "--output", + "{output}" + ], + "note": "Actual production-body host boundaries and runtime mutants; no firmware or hardware qualification." + }, + { + "name": "tools/asahi/t6030-display-admission-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/t6030-display-admission-tests.py", + "--mutations", + "--output", + "{output}" + ], + "note": "Actual production-body host boundaries and runtime mutants; no firmware or hardware qualification." + }, + { + "name": "tools/asahi/t6030-display-linkage-tests.py", + "group": "tools", + "setup": [ + [ + "make", + "-s", + "O={output}-conf", + "allnoconfig" + ] + ], + "argv": [ + "python3", + "tools/asahi/t6030-display-linkage-tests.py", + "--conf", + "{output}-conf/scripts/kconfig/conf", + "--output", + "{output}" + ], + "note": "Build only the host Kconfig tool in an isolated output directory; check all built-in/module dependency combinations." + }, + { + "name": "tools/asahi/test-j613-dcp-power.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/test-j613-dcp-power.py" + ] + }, + { + "name": "tools/asahi/test-m3-mailbox.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/test-m3-mailbox.py" + ] + }, + { + "name": "tools/asahi/m3-firmware-tests.rs", + "group": "tools", + "argv": [ + "{output}", + "--test-threads=1" + ], + "setup": [ + [ + "rustc", + "--edition=2021", + "--test", + "tools/asahi/m3-firmware-tests.rs", + "-o", + "{output}" + ] + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/test-m3-uapi.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/test-m3-uapi.py" + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/test-m3-power.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/test-m3-power.py" + ], + "rust": "1.85.0" + }, + { + "name": "tools/asahi/dcpext26-wire-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-wire-tests.py" + ], + "note": "Production native26 external encoder against full byte fixtures and malformed reply/geometry cases; no firmware execution." + }, + { + "name": "tools/asahi/pmp-native26-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/pmp-native26-tests.py" + ], + "note": "Actual native26 report admission on synthetic board/OF/table inputs; no MMIO or firmware execution." + }, + { + "name": "tools/asahi/dcpext26-profile-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-profile-tests.py" + ], + "note": "Actual native26 callback helpers, startup peer and causal readiness; no hardware qualification." + }, + { + "name": "tools/asahi/dcpext26-lifecycle-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-lifecycle-tests.py" + ], + "note": "Actual retained-stop and control bodies with fault peer, host-drain and epoch budget fixtures." + }, + { + "name": "tools/asahi/dcpext26-edid-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-edid-tests.py" + ], + "note": "Actual native AV identity/generation/owner predicates and EDID wire header, length and checksums." + }, + { + "name": "tools/asahi/dcpext26-admission-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-admission-tests.py" + ], + "note": "Actual native identity and powered RUN/locked-DART admission predicates with bit and malformed-profile controls." + }, + { + "name": "tools/asahi/dcpext26-present-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-present-tests.py" + ], + "note": "Actual primary DRM callbacks: native start/submit rejection is terminal; accepted early D590 retains original receive time; classic behavior stays unchanged." + }, + { + "name": "tools/asahi/dcpext26-changeset-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-changeset-tests.py" + ], + "note": "Actual native property builder plus Linux OF update/remove/inversion bodies: absent/existing/equal values and rollback; ASan/UBSan allocation and queue failures." + }, + { + "name": "tools/asahi/dcpext26-startup-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-startup-tests.py" + ] + }, + { + "name": "tools/asahi/dcpext26-modeset-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-modeset-tests.py" + ] + }, + { + "name": "tools/asahi/dcpext26-parity-tests.py", + "group": "tools", + "argv": [ + "python3", + "tools/asahi/dcpext26-parity-tests.py" + ] + } + ], + "helpers": { + "tools/testing/selftests/drivers/gpu/apple-dcp/run.py": "the implementation behind tools/testing/selftests/drivers/gpu/apple-dcp/run.sh, which is listed", + "tools/testing/selftests/drivers/gpu/asahi-g15/uat-owner.c": "compiled and executed by the registered asahi-g15/run.py actual reservation-owner fixture", + "tools/testing/selftests/drivers/gpu/asahi-g15/pmp-bridge.c": "compiled and executed by the registered asahi-g15/run.py maintained PMP fixture", + "tools/asahi/t6030-pmp-graph-tests.c": "compiled and executed by the corresponding registered Python actual-body host suite", + "tools/asahi/t6030-display-admission-tests.c": "compiled and executed by the corresponding registered Python actual-body host suite", + "tools/asahi/m3-uapi-tests.rs": "compiled and run by the registered tools/asahi/test-m3-uapi.py C8 UAPI fixture" + }, + "excluded": { + "tools/aurora-sep/tests/coordinator/runtime-check.sh": "Needs /dev/sep-bio, a running kernel booted with trusted.source=applesep, the hw_random sysfs interface and fprintd. Coordinator payload for an approved device.", + "tools/aurora-sep/tests/coordinator/deletion-check.sh": "Needs /dev/sep-bio, fprintd over the system bus and a previously enrolled expendable test account; spans a reboot.", + "tools/aurora-sep/tests/coordinator/sep-live-count.py": "Opens /dev/sep-bio on a running kernel. Not picked up by `unittest discover` (no test_ prefix) - keep it that way.", + "tools/aurora-wake/s2idle-rtc-regression.sh": "Real suspend-to-idle cycles with rtcwake on an Apple machine, as root. Its argument checks run hermetically via test-s2idle-rtc-regression.sh (in suites.json).", + "tools/aurora-wake/attended-bt-wake.sh": "J700 with a paired Bluetooth keyboard, a person pressing keys, root.", + "tools/aurora-wake/bt-wake-cycle.sh": "One s2idle cycle on a J700 with Bluetooth wake, root.", + "tools/aurora-wake/bt-wake-power.sh": "Alternating RTC-timed sleeps on a J700, root.", + "tools/aurora-wake/bt-wake-emulated.sh": "Drives two other machines over SSH (target and a BlueZ host).", + "tools/aurora-wake/hog-keyboard.py": "BLE HID keyboard emulator for a second BlueZ host (system bus + Bluetooth adapter).", + "tools/aurora-tb/tb-pcie-report": "Reads live Thunderbolt/PCIe state and the kernel log with a dock attached (may need sudo).", + "tools/aurora-m3/m3-serial.py": "macOS helper (sudo, macvdmtool) recording a second Mac's serial console over a USB-C cable.", + "tools/testing/selftests/drivers/apple/j813-peripherals.sh": "Physical J813 check (exits 4 unless /proc/device-tree/compatible has apple,j813 and euid 0).", + "tools/testing/selftests/drivers/net/mt7932_e2e.py": "End-to-end Wi-Fi on a J700 via nmcli and the real network. Its helper unit tests (mt7932_e2e_test.py) are in suites.json.", + "tools/testing/selftests/arm64/user4k/": "kselftest programs for the user4k granule API: need an arm64 Aurora kernel booted with user4k=on (and an aarch64 toolchain to build). check-any-quarter.py / runtime-app.py consume output of those programs.", + "tools/aurora-sep/recipes/mesa-m3/tests/test-probe.sh": "Needs a built mesa-m3-probe (C, links Mesa/EGL/Wayland) and a headless weston for the wayland case.", + "tools/aurora-sep/recipes/mesa-m3/tests/test-confirm.sh": "Needs a built mesa-m3-wlcheck (Wayland client) passed as its argument.", + "tools/aurora-sep/recipes/mesa-m3/tests/build-checks.sh": "Runs inside the PKGBUILD package() step against a freshly built Mesa tree (makepkg, aarch64).", + "tools/aurora-bindgen/build.py": "Downloads the pinned bindgen 0.73.2 source archive (codeload.github.com) and crates.io dependencies, then cargo build --locked -j2 (89 s here incl. its own test.py run; passed). Viable as an optional cached job; GitHub runners have network.", + "tools/aurora-bindgen/test.py": "Hermetic once a patched bindgen exists (--bindgen PATH, 2 s, passed). --baseline-bindgen must be an unpatched 0.73.2; the system 0.73.1 fails the tag-only equality check by design.", + "tools/asahi-m3/completion_progress.py, fault_snapshot.py, retirement_progress.py": "Libraries that read /sys/kernel/debug/asahi-m3/* or /dev/kmsg on an M3 machine. Their offline tests are in suites.json.", + "tools/aurora-sep/air-gpu/*.sh, tools/aurora-sep/pro/*, tools/aurora-sep/neo/*": "On-device helpers; exercised through fixtures by tools/aurora-sep/tests (test_air_gpu, test_m3_pro_perf, test_neo_gpu).", + "drivers/**/*-test.c, *_test.c, tools/testing/kunit/configs/{apple_firmware,arm64_user4k*}.config": "KUnit suites: need a kernel build and tools/testing/kunit/kunit.py (arm64 under QEMU). Not host scripts.", + "tools/aurora-sep/tests: env-gated cases": "Skipped unless set: AURORA_ADT_FIXTURE (real ADT dump), AIR_GPU_TEST_VULKAN=1 (real Vulkan job on a render node), AURORA_PRO_MESA_PKG / AURORA_M1N1_PKG (release packages); test_m3max_kit real-package cases need /nonexistent/... packages. The rest of each module runs." } - ], - "helpers": { - "tools/testing/selftests/drivers/gpu/apple-dcp/run.py": "the implementation behind tools/testing/selftests/drivers/gpu/apple-dcp/run.sh, which is listed", - "tools/testing/selftests/drivers/gpu/asahi-g15/uat-owner.c": "compiled and executed by the registered asahi-g15/run.py actual reservation-owner fixture", - "tools/testing/selftests/drivers/gpu/asahi-g15/pmp-bridge.c": "compiled and executed by the registered asahi-g15/run.py maintained PMP fixture", - "tools/asahi/t6030-pmp-graph-tests.c": "compiled and executed by the corresponding registered Python actual-body host suite", - "tools/asahi/t6030-display-admission-tests.c": "compiled and executed by the corresponding registered Python actual-body host suite" - }, - "excluded": { - "tools/aurora-sep/tests/coordinator/runtime-check.sh": "Needs /dev/sep-bio, a running kernel booted with trusted.source=applesep, the hw_random sysfs interface and fprintd. Coordinator payload for an approved device.", - "tools/aurora-sep/tests/coordinator/deletion-check.sh": "Needs /dev/sep-bio, fprintd over the system bus and a previously enrolled expendable test account; spans a reboot.", - "tools/aurora-sep/tests/coordinator/sep-live-count.py": "Opens /dev/sep-bio on a running kernel. Not picked up by `unittest discover` (no test_ prefix) - keep it that way.", - "tools/aurora-wake/s2idle-rtc-regression.sh": "Real suspend-to-idle cycles with rtcwake on an Apple machine, as root. Its argument checks run hermetically via test-s2idle-rtc-regression.sh (in suites.json).", - "tools/aurora-wake/attended-bt-wake.sh": "J700 with a paired Bluetooth keyboard, a person pressing keys, root.", - "tools/aurora-wake/bt-wake-cycle.sh": "One s2idle cycle on a J700 with Bluetooth wake, root.", - "tools/aurora-wake/bt-wake-power.sh": "Alternating RTC-timed sleeps on a J700, root.", - "tools/aurora-wake/bt-wake-emulated.sh": "Drives two other machines over SSH (target and a BlueZ host).", - "tools/aurora-wake/hog-keyboard.py": "BLE HID keyboard emulator for a second BlueZ host (system bus + Bluetooth adapter).", - "tools/aurora-tb/tb-pcie-report": "Reads live Thunderbolt/PCIe state and the kernel log with a dock attached (may need sudo).", - "tools/aurora-m3/m3-serial.py": "macOS helper (sudo, macvdmtool) recording a second Mac's serial console over a USB-C cable.", - "tools/testing/selftests/drivers/apple/j813-peripherals.sh": "Physical J813 check (exits 4 unless /proc/device-tree/compatible has apple,j813 and euid 0).", - "tools/testing/selftests/drivers/net/mt7932_e2e.py": "End-to-end Wi-Fi on a J700 via nmcli and the real network. Its helper unit tests (mt7932_e2e_test.py) are in suites.json.", - "tools/testing/selftests/arm64/user4k/": "kselftest programs for the user4k granule API: need an arm64 Aurora kernel booted with user4k=on (and an aarch64 toolchain to build). check-any-quarter.py / runtime-app.py consume output of those programs.", - "tools/aurora-sep/recipes/mesa-m3/tests/test-probe.sh": "Needs a built mesa-m3-probe (C, links Mesa/EGL/Wayland) and a headless weston for the wayland case.", - "tools/aurora-sep/recipes/mesa-m3/tests/test-confirm.sh": "Needs a built mesa-m3-wlcheck (Wayland client) passed as its argument.", - "tools/aurora-sep/recipes/mesa-m3/tests/build-checks.sh": "Runs inside the PKGBUILD package() step against a freshly built Mesa tree (makepkg, aarch64).", - "tools/aurora-bindgen/build.py": "Downloads the pinned bindgen 0.73.2 source archive (codeload.github.com) and crates.io dependencies, then cargo build --locked -j2 (89 s here incl. its own test.py run; passed). Viable as an optional cached job; GitHub runners have network.", - "tools/aurora-bindgen/test.py": "Hermetic once a patched bindgen exists (--bindgen PATH, 2 s, passed). --baseline-bindgen must be an unpatched 0.73.2; the system 0.73.1 fails the tag-only equality check by design.", - "tools/asahi-m3/completion_progress.py, fault_snapshot.py, retirement_progress.py": "Libraries that read /sys/kernel/debug/asahi-m3/* or /dev/kmsg on an M3 machine. Their offline tests are in suites.json.", - "tools/aurora-sep/air-gpu/*.sh, tools/aurora-sep/pro/*, tools/aurora-sep/neo/*": "On-device helpers; exercised through fixtures by tools/aurora-sep/tests (test_air_gpu, test_m3_pro_perf, test_neo_gpu).", - "drivers/**/*-test.c, *_test.c, tools/testing/kunit/configs/{apple_firmware,arm64_user4k*}.config": "KUnit suites: need a kernel build and tools/testing/kunit/kunit.py (arm64 under QEMU). Not host scripts.", - "tools/aurora-sep/tests: env-gated cases": "Skipped unless set: AURORA_ADT_FIXTURE (real ADT dump), AIR_GPU_TEST_VULKAN=1 (real Vulkan job on a render node), AURORA_PRO_MESA_PKG / AURORA_M1N1_PKG (release packages); test_m3max_kit real-package cases need /nonexistent/... packages. The rest of each module runs." - } } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/common.py b/tools/testing/selftests/drivers/gpu/apple-dcp/common.py index 4097d7db7d86ae..7a4ba475b75b4c 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/common.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/common.py @@ -61,10 +61,68 @@ def tipd_pm_header(root): '#define clamp(val, lo, hi) ({ __auto_type _v = (val); ' '__typeof__(_v) _lo = (lo), _hi = (hi); _v < _lo ? _lo : _v > _hi ? _hi : _v; })') +def classic_unit(unit): + # This suite exercises established profiles. Native26 has separate actual-body + # fixtures; model its optional branch and storage without changing assertions. + if 'dcp_is_ext26(' in unit and not re.search(r'^static [^\n]+ dcp_is_ext26\(', unit, re.M): + unit = '#define dcp_is_ext26(d) 0\n' + unit + # Native-only diagnostic/OPEN branches still need declarations when parsed. + for token, value in (('SYSTEM_ENDPOINT', '0x20'), ('DISP0_ENDPOINT', '0x23')): + if token in unit and not re.search(r'#define ' + token + r'\s', unit): + unit = '#define ' + token + ' ' + value + '\n' + unit + if 'dptx_native_controllers_ready(' in unit: + unit = '#define dptx_native_controllers_ready(d) 0\n' + unit + if 'dcp_is_retained_external(' in unit and not re.search(r'^(?:static )?bool dcp_is_retained_external\(', unit, re.M): + predicate = 'dcp_is_external_h17p(d)' if 'dcp_is_external_h17p(' in unit else '((d)->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL)' + unit = '#define dcp_is_retained_external(d) ' + predicate + '\n' + unit + if 'ext26_' in unit and 'struct apple_dcp {' in unit: + mock = block(unit, 'struct apple_dcp {') + fields = [] + for name in ('ext26_pm_preparing', 'ext26_stopping', 'ext26_normal', 'ext26_blocked', 'external_stopped', 'crashed', 'ext26_services_open', 'ext26_client_open', 'ext26_edid_tried'): + if name not in mock: + fields.append('bool ' + name + ';') + if 'ext26_edid_generation' not in mock: + fields.append('u64 ext26_edid_generation;') + if 'ext26_aux' not in mock: + fields.append('struct apple_dcp_afkep *ext26_aux[3];') + if 'ext26_blank_work' not in mock: + fields.append('int ext26_blank_work;') + for name in ('systemep', 'ibootep', 'dptx_controller_ep', 'dptxep', 'dcpavservep'): + if 'dcp->' + name in unit and not re.search(r'\b' + name + r'\b', mock): + fields.append('struct apple_dcp_afkep *' + name + ';') + if 'dcp->rtk' in unit and not re.search(r'\brtk\b', mock): + fields.append('void *rtk;') + unit = unit.replace('struct apple_dcp {', 'struct apple_dcp {\n' + '\n'.join(fields), 1) + if 'struct apple_dcp_afkep *endpoints' in unit and 'struct apple_dcp_afkep {' not in unit: + unit = 'struct apple_dcp_afkep { void *wq; };\n' + unit + for name, expression in (('afk_host_idle', '1'), ('iomfb_ext26_retain_epoch', '0'), + ('apple_rtkit_flush_rx', '((void)0)'), ('flush_workqueue', '((void)0)'), + ('flush_work', '((void)0)'), ('queue_work', '0'), + ('dcp_modes_begin_attachment', '((void)0)')): + if name + '(' in unit and not re.search(r'^(?:static )?[^\n;]+\b' + name + r'\([^;]*\)\s*\{', unit, re.M): + unit = '#define ' + name + '(...) ' + expression + '\n' + unit + if 'struct apple_dcp_afkep *endpoints' in unit: + unit = '#ifndef ARRAY_SIZE\n#define ARRAY_SIZE(a) (sizeof(a)/sizeof(*(a)))\n#endif\n#ifndef IS_ERR_OR_NULL\n#define IS_ERR_OR_NULL(p) (!(p))\n#endif\n' + unit + if 'WRITE_ONCE(' in unit and '#define WRITE_ONCE' not in unit: + unit = '#define WRITE_ONCE(x,v) ((x)=(v))\n' + unit + if 'DPAVSERV_ENDPOINT' in unit and not re.search(r'#define DPAVSERV_ENDPOINT\s', unit): + unit = '#define DPAVSERV_ENDPOINT 0x28\n' + unit + if 'dcp_direct_crossbar_link(' in unit and not re.search(r'^(?:static )?[^\n;]+\bdcp_direct_crossbar_link\([^;]*\)\s*\{', unit, re.M): + unit = '#define dcp_direct_crossbar_link(d,on) 0\n' + unit + if 'ext26_prop_start(' in unit: + unit = '#define ext26_prop_start(d,n) 0\n' + unit + if 'dcp->chunks.received' in unit: + unit = unit.replace('void *data;\n size_t length;', 'void *data;\n size_t received; bool contiguous; size_t length;') + if 'ext26_command_bytes' in unit: + unit = unit.replace('struct apple_dcp_afkep {', 'struct apple_dcp_afkep { size_t ext26_command_bytes; unsigned ext26_command_count;', 1) + unit = '#ifndef ALIGN\n#define ALIGN(x,a) (((x)+(a)-1)&~((a)-1))\n#endif\n#ifndef SZ_16K\n#define SZ_16K 16384UL\n#endif\n#ifndef SZ_16M\n#define SZ_16M (16UL<<20)\n#endif\n' + unit + return unit + def run_unit(name, unit, output, cases=((),), includes=(), extra=()): work = output / name work.mkdir(parents=True, exist_ok=True) source = work / 'host.c' + unit = classic_unit(unit) source.write_text(unit) command = shlex.split(os.environ.get('HOSTCC', 'clang')) + [ '-std=gnu11', '-Wall', '-Werror', '-O1', '-g', diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py b/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py index 1cee66408389df..d62d5d529a223d 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/edid.py @@ -848,9 +848,9 @@ def function(text, signature): struct { struct mutex lock; bool stopped; void *owner; } iomfb; }; struct afk_receive_message_work { struct apple_dcp_afkep *ep; u64 message; struct work_struct work; }; -struct apple_rtkit_ops { void (*recv_message)(void *,u8,u64); }; +struct apple_rtkit_ops { void (*recv_message)(void *,u8,u64); void (*recv_message_timed)(void *,u8,u64,u64); }; struct apple_rtkit { bool shutting_down, sync_syslog_ack; const struct apple_rtkit_ops *ops; void *cookie,*dev; }; -struct apple_rtkit_rx_work { struct apple_rtkit *rtk; u8 ep; u64 msg; struct work_struct work; }; +struct apple_rtkit_rx_work { u64 received_ns; struct apple_rtkit *rtk; u8 ep; u64 msg; struct work_struct work; }; #define current ((void *)1) static pthread_mutex_t reply_lock=PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t reply_ready=PTHREAD_COND_INITIALIZER; diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index 96fd0b5dcea014..8ed4875f713f03 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -1340,7 +1340,8 @@ def function(source, marker): # The T8122 dcpext wrapper moved the pipe's probe into dcp_platform_probe_pipe(). probe = function(dcp, 'static int dcp_platform_probe_pipe(' if 'static int dcp_platform_probe_pipe(' in dcp else 'static int dcp_platform_probe(') - publish = probe[probe.rindex('\tif (dcp->hw.iomfb_method_profile ==\n\t DCP_IOMFB_METHODS_H17P_EXTERNAL) {'):] + publish_marker = '\tif (dcp_is_retained_external(dcp)) {' if 'dcp_is_retained_external(dcp)' in probe else '\tif (dcp->hw.iomfb_method_profile ==\n\t DCP_IOMFB_METHODS_H17P_EXTERNAL) {' + publish = probe[probe.rindex(publish_marker):] # Which processor must come up before the boot framebuffer is taken over. header = root / 'drivers/gpu/drm/apple/apple_boot_display.h' boot_display = '' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py b/tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py index e7b12e5bdbb59f..c00431b1e9b53b 100755 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/plane-layout.py @@ -110,15 +110,26 @@ def unit(root, ref=None, mutation=None, callback_ref=None): struct drm_rect { int x1,y1,x2,y2; }; enum drm_color_range { DRM_COLOR_YCBCR_LIMITED_RANGE, DRM_COLOR_YCBCR_FULL_RANGE }; enum drm_color_encoding { DRM_COLOR_YCBCR_BT601, DRM_COLOR_YCBCR_BT709, DRM_COLOR_YCBCR_BT2020 }; -struct drm_crtc_state { struct { int hdisplay,vdisplay; } mode; }; -struct drm_plane_state { void *crtc; bool visible; struct drm_framebuffer *fb; struct drm_rect src,dst; enum drm_color_range color_range; enum drm_color_encoding color_encoding; }; +struct drm_crtc_state { struct { int hdisplay,vdisplay; } mode; bool vrr_enabled; }; +struct drm_plane_state { u32 rotation,src_x,src_y,src_w,src_h; int crtc_x,crtc_y,crtc_w,crtc_h; void *crtc; bool visible; struct drm_framebuffer *fb; struct drm_rect src,dst; enum drm_color_range color_range; enum drm_color_encoding color_encoding; }; struct drm_plane; struct drm_plane_funcs { bool (*format_mod_supported)(struct drm_plane *,u32,u64); }; -struct drm_plane { struct { u32 id; } base;struct drm_plane_state *state;u32 *format_types;unsigned int format_count;u64 *modifiers;unsigned int modifier_count;const struct drm_plane_funcs *funcs; }; +struct drm_plane { int type; struct { u32 id; } base;struct drm_plane_state *state;u32 *format_types;unsigned int format_count;u64 *modifiers;unsigned int modifier_count;const struct drm_plane_funcs *funcs; }; struct drm_atomic_commit { struct { void *dev; } *dev; }; ''' prefix += block(plane_header, 'struct apple_plane_state {') + ';\n' prefix += r''' +/* Layout controls use the legacy DCP; ext26 admission has its own suite. */ +#define DRM_PLANE_TYPE_PRIMARY 1 +#define DRM_MODE_ROTATE_0 1 +struct platform_device { int unused; }; +struct apple_dcp { int unused; }; +struct apple_crtc { struct platform_device *dcp; }; +static struct apple_crtc host_crtc; +static struct apple_crtc *to_apple_crtc(void *crtc) { return &host_crtc; } +static struct apple_dcp *platform_get_drvdata(struct platform_device *pdev) { return NULL; } +static bool dcp_is_ext26(struct apple_dcp *dcp) { return false; } +static bool iomfb_ext26_backing_fresh(struct apple_dcp *dcp,struct drm_framebuffer *fb) { return true; } #define to_apple_plane_state(p) container_of(p,struct apple_plane_state,base) static struct drm_crtc_state crtc_state = { .mode = { 3840,2160 } }; static struct drm_plane_state *drm_atomic_get_new_plane_state(struct drm_atomic_commit *c,struct drm_plane *p) { return p->state; } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py b/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py index 0a26ac0e584f13..75bd0c64578520 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/recovery.py @@ -7,6 +7,7 @@ """ from pathlib import Path import json, hashlib, subprocess +from common import classic_unit def check(root, output): import edid @@ -264,6 +265,7 @@ def extract(text,signature): includes=[root/'drivers/gpu/drm/apple',root/'tools/testing/selftests/drivers/gpu/apple-afkv2/host-include'] results=[] def run(name,text,cases): + text=classic_unit(text) area=out/name;area.mkdir(exist_ok=True);(area/'host.c').write_text(text) cmd=['clang','-std=gnu11','-Wall','-Werror','-Wno-unused-function','-O1','-g','-fsanitize=address,undefined','-fno-omit-frame-pointer']+['-I'+str(p) for p in includes]+[str(area/'host.c'),'-o',str(area/'host')] (area/'command.json').write_text(json.dumps(cmd,indent=2)+'\n')