diff options
Diffstat (limited to 'drivers/soc')
-rw-r--r-- | drivers/soc/qcom/smd.c | 229 | ||||
-rw-r--r-- | drivers/soc/qcom/spm.c | 8 | ||||
-rw-r--r-- | drivers/soc/rockchip/pm_domains.c | 142 |
3 files changed, 312 insertions, 67 deletions
diff --git a/drivers/soc/qcom/smd.c b/drivers/soc/qcom/smd.c index 498fd05..b6434c4 100644 --- a/drivers/soc/qcom/smd.c +++ b/drivers/soc/qcom/smd.c @@ -106,9 +106,9 @@ static const struct { * @channels: list of all channels detected on this edge * @channels_lock: guard for modifications of @channels * @allocated: array of bitmaps representing already allocated channels - * @need_rescan: flag that the @work needs to scan smem for new channels * @smem_available: last available amount of smem triggering a channel scan - * @work: work item for edge house keeping + * @scan_work: work item for discovering new channels + * @state_work: work item for edge state changes */ struct qcom_smd_edge { struct qcom_smd *smd; @@ -127,10 +127,12 @@ struct qcom_smd_edge { DECLARE_BITMAP(allocated[SMD_ALLOC_TBL_COUNT], SMD_ALLOC_TBL_SIZE); - bool need_rescan; unsigned smem_available; - struct work_struct work; + wait_queue_head_t new_channel_event; + + struct work_struct scan_work; + struct work_struct state_work; }; /* @@ -186,13 +188,14 @@ struct qcom_smd_channel { int fifo_size; void *bounce_buffer; - int (*cb)(struct qcom_smd_device *, const void *, size_t); + qcom_smd_cb_t cb; spinlock_t recv_lock; int pkt_size; struct list_head list; + struct list_head dev_list; }; /** @@ -378,6 +381,19 @@ static void qcom_smd_channel_reset(struct qcom_smd_channel *channel) } /* + * Set the callback for a channel, with appropriate locking + */ +static void qcom_smd_channel_set_callback(struct qcom_smd_channel *channel, + qcom_smd_cb_t cb) +{ + unsigned long flags; + + spin_lock_irqsave(&channel->recv_lock, flags); + channel->cb = cb; + spin_unlock_irqrestore(&channel->recv_lock, flags); +}; + +/* * Calculate the amount of data available in the rx fifo */ static size_t qcom_smd_channel_get_rx_avail(struct qcom_smd_channel *channel) @@ -601,7 +617,8 @@ static irqreturn_t qcom_smd_edge_intr(int irq, void *data) struct qcom_smd_edge *edge = data; struct qcom_smd_channel *channel; unsigned available; - bool kick_worker = false; + bool kick_scanner = false; + bool kick_state = false; /* * Handle state changes or data on each of the channels on this edge @@ -609,7 +626,7 @@ static irqreturn_t qcom_smd_edge_intr(int irq, void *data) spin_lock(&edge->channels_lock); list_for_each_entry(channel, &edge->channels, list) { spin_lock(&channel->recv_lock); - kick_worker |= qcom_smd_channel_intr(channel); + kick_state |= qcom_smd_channel_intr(channel); spin_unlock(&channel->recv_lock); } spin_unlock(&edge->channels_lock); @@ -622,12 +639,13 @@ static irqreturn_t qcom_smd_edge_intr(int irq, void *data) available = qcom_smem_get_free_space(edge->remote_pid); if (available != edge->smem_available) { edge->smem_available = available; - edge->need_rescan = true; - kick_worker = true; + kick_scanner = true; } - if (kick_worker) - schedule_work(&edge->work); + if (kick_scanner) + schedule_work(&edge->scan_work); + if (kick_state) + schedule_work(&edge->state_work); return IRQ_HANDLED; } @@ -793,18 +811,12 @@ static int qcom_smd_dev_match(struct device *dev, struct device_driver *drv) } /* - * Probe the smd client. - * - * The remote side have indicated that it want the channel to be opened, so - * complete the state handshake and probe our client driver. + * Helper for opening a channel */ -static int qcom_smd_dev_probe(struct device *dev) +static int qcom_smd_channel_open(struct qcom_smd_channel *channel, + qcom_smd_cb_t cb) { - struct qcom_smd_device *qsdev = to_smd_device(dev); - struct qcom_smd_driver *qsdrv = to_smd_driver(dev); - struct qcom_smd_channel *channel = qsdev->channel; size_t bb_size; - int ret; /* * Packets are maximum 4k, but reduce if the fifo is smaller @@ -814,12 +826,44 @@ static int qcom_smd_dev_probe(struct device *dev) if (!channel->bounce_buffer) return -ENOMEM; - channel->cb = qsdrv->callback; - + qcom_smd_channel_set_callback(channel, cb); qcom_smd_channel_set_state(channel, SMD_CHANNEL_OPENING); - qcom_smd_channel_set_state(channel, SMD_CHANNEL_OPENED); + return 0; +} + +/* + * Helper for closing and resetting a channel + */ +static void qcom_smd_channel_close(struct qcom_smd_channel *channel) +{ + qcom_smd_channel_set_callback(channel, NULL); + + kfree(channel->bounce_buffer); + channel->bounce_buffer = NULL; + + qcom_smd_channel_set_state(channel, SMD_CHANNEL_CLOSED); + qcom_smd_channel_reset(channel); +} + +/* + * Probe the smd client. + * + * The remote side have indicated that it want the channel to be opened, so + * complete the state handshake and probe our client driver. + */ +static int qcom_smd_dev_probe(struct device *dev) +{ + struct qcom_smd_device *qsdev = to_smd_device(dev); + struct qcom_smd_driver *qsdrv = to_smd_driver(dev); + struct qcom_smd_channel *channel = qsdev->channel; + int ret; + + ret = qcom_smd_channel_open(channel, qsdrv->callback); + if (ret) + return ret; + ret = qsdrv->probe(qsdev); if (ret) goto err; @@ -831,11 +875,7 @@ static int qcom_smd_dev_probe(struct device *dev) err: dev_err(&qsdev->dev, "probe failed\n"); - channel->cb = NULL; - kfree(channel->bounce_buffer); - channel->bounce_buffer = NULL; - - qcom_smd_channel_set_state(channel, SMD_CHANNEL_CLOSED); + qcom_smd_channel_close(channel); return ret; } @@ -850,16 +890,15 @@ static int qcom_smd_dev_remove(struct device *dev) struct qcom_smd_device *qsdev = to_smd_device(dev); struct qcom_smd_driver *qsdrv = to_smd_driver(dev); struct qcom_smd_channel *channel = qsdev->channel; - unsigned long flags; + struct qcom_smd_channel *tmp; + struct qcom_smd_channel *ch; qcom_smd_channel_set_state(channel, SMD_CHANNEL_CLOSING); /* * Make sure we don't race with the code receiving data. */ - spin_lock_irqsave(&channel->recv_lock, flags); - channel->cb = NULL; - spin_unlock_irqrestore(&channel->recv_lock, flags); + qcom_smd_channel_set_callback(channel, NULL); /* Wake up any sleepers in qcom_smd_send() */ wake_up_interruptible(&channel->fblockread_event); @@ -872,15 +911,14 @@ static int qcom_smd_dev_remove(struct device *dev) qsdrv->remove(qsdev); /* - * The client is now gone, cleanup and reset the channel state. + * The client is now gone, close and release all channels associated + * with this sdev */ - channel->qsdev = NULL; - kfree(channel->bounce_buffer); - channel->bounce_buffer = NULL; - - qcom_smd_channel_set_state(channel, SMD_CHANNEL_CLOSED); - - qcom_smd_channel_reset(channel); + list_for_each_entry_safe(ch, tmp, &channel->dev_list, dev_list) { + qcom_smd_channel_close(ch); + list_del(&ch->dev_list); + ch->qsdev = NULL; + } return 0; } @@ -1006,6 +1044,77 @@ void qcom_smd_driver_unregister(struct qcom_smd_driver *qsdrv) } EXPORT_SYMBOL(qcom_smd_driver_unregister); +static struct qcom_smd_channel * +qcom_smd_find_channel(struct qcom_smd_edge *edge, const char *name) +{ + struct qcom_smd_channel *channel; + struct qcom_smd_channel *ret = NULL; + unsigned long flags; + unsigned state; + + spin_lock_irqsave(&edge->channels_lock, flags); + list_for_each_entry(channel, &edge->channels, list) { + if (strcmp(channel->name, name)) + continue; + + state = GET_RX_CHANNEL_INFO(channel, state); + if (state != SMD_CHANNEL_OPENING && + state != SMD_CHANNEL_OPENED) + continue; + + ret = channel; + break; + } + spin_unlock_irqrestore(&edge->channels_lock, flags); + + return ret; +} + +/** + * qcom_smd_open_channel() - claim additional channels on the same edge + * @sdev: smd_device handle + * @name: channel name + * @cb: callback method to use for incoming data + * + * Returns a channel handle on success, or -EPROBE_DEFER if the channel isn't + * ready. + */ +struct qcom_smd_channel *qcom_smd_open_channel(struct qcom_smd_device *sdev, + const char *name, + qcom_smd_cb_t cb) +{ + struct qcom_smd_channel *channel; + struct qcom_smd_edge *edge = sdev->channel->edge; + int ret; + + /* Wait up to HZ for the channel to appear */ + ret = wait_event_interruptible_timeout(edge->new_channel_event, + (channel = qcom_smd_find_channel(edge, name)) != NULL, + HZ); + if (!ret) + return ERR_PTR(-ETIMEDOUT); + + if (channel->state != SMD_CHANNEL_CLOSED) { + dev_err(&sdev->dev, "channel %s is busy\n", channel->name); + return ERR_PTR(-EBUSY); + } + + channel->qsdev = sdev; + ret = qcom_smd_channel_open(channel, cb); + if (ret) { + channel->qsdev = NULL; + return ERR_PTR(ret); + } + + /* + * Append the list of channel to the channels associated with the sdev + */ + list_add_tail(&channel->dev_list, &sdev->channel->dev_list); + + return channel; +} +EXPORT_SYMBOL(qcom_smd_open_channel); + /* * Allocate the qcom_smd_channel object for a newly found smd channel, * retrieving and validating the smem items involved. @@ -1027,6 +1136,7 @@ static struct qcom_smd_channel *qcom_smd_create_channel(struct qcom_smd_edge *ed if (!channel) return ERR_PTR(-ENOMEM); + INIT_LIST_HEAD(&channel->dev_list); channel->edge = edge; channel->name = devm_kstrdup(smd->dev, name, GFP_KERNEL); if (!channel->name) @@ -1089,8 +1199,9 @@ free_name_and_channel: * qcom_smd_create_channel() to create representations of these and add * them to the edge's list of channels. */ -static void qcom_discover_channels(struct qcom_smd_edge *edge) +static void qcom_channel_scan_worker(struct work_struct *work) { + struct qcom_smd_edge *edge = container_of(work, struct qcom_smd_edge, scan_work); struct qcom_smd_alloc_entry *alloc_tbl; struct qcom_smd_alloc_entry *entry; struct qcom_smd_channel *channel; @@ -1140,10 +1251,12 @@ static void qcom_discover_channels(struct qcom_smd_edge *edge) dev_dbg(smd->dev, "new channel found: '%s'\n", channel->name); set_bit(i, edge->allocated[tbl]); + + wake_up_interruptible(&edge->new_channel_event); } } - schedule_work(&edge->work); + schedule_work(&edge->state_work); } /* @@ -1151,29 +1264,23 @@ static void qcom_discover_channels(struct qcom_smd_edge *edge) * then scans all registered channels for state changes that should be handled * by creating or destroying smd client devices for the registered channels. * - * LOCKING: edge->channels_lock is not needed to be held during the traversal - * of the channels list as it's done synchronously with the only writer. + * LOCKING: edge->channels_lock only needs to cover the list operations, as the + * worker is killed before any channels are deallocated */ static void qcom_channel_state_worker(struct work_struct *work) { struct qcom_smd_channel *channel; struct qcom_smd_edge *edge = container_of(work, struct qcom_smd_edge, - work); + state_work); unsigned remote_state; - - /* - * Rescan smem if we have reason to belive that there are new channels. - */ - if (edge->need_rescan) { - edge->need_rescan = false; - qcom_discover_channels(edge); - } + unsigned long flags; /* * Register a device for any closed channel where the remote processor * is showing interest in opening the channel. */ + spin_lock_irqsave(&edge->channels_lock, flags); list_for_each_entry(channel, &edge->channels, list) { if (channel->state != SMD_CHANNEL_CLOSED) continue; @@ -1183,7 +1290,9 @@ static void qcom_channel_state_worker(struct work_struct *work) remote_state != SMD_CHANNEL_OPENED) continue; + spin_unlock_irqrestore(&edge->channels_lock, flags); qcom_smd_create_device(channel); + spin_lock_irqsave(&edge->channels_lock, flags); } /* @@ -1200,8 +1309,11 @@ static void qcom_channel_state_worker(struct work_struct *work) remote_state == SMD_CHANNEL_OPENED) continue; + spin_unlock_irqrestore(&edge->channels_lock, flags); qcom_smd_destroy_device(channel); + spin_lock_irqsave(&edge->channels_lock, flags); } + spin_unlock_irqrestore(&edge->channels_lock, flags); } /* @@ -1219,7 +1331,8 @@ static int qcom_smd_parse_edge(struct device *dev, INIT_LIST_HEAD(&edge->channels); spin_lock_init(&edge->channels_lock); - INIT_WORK(&edge->work, qcom_channel_state_worker); + INIT_WORK(&edge->scan_work, qcom_channel_scan_worker); + INIT_WORK(&edge->state_work, qcom_channel_state_worker); edge->of_node = of_node_get(node); @@ -1303,13 +1416,13 @@ static int qcom_smd_probe(struct platform_device *pdev) for_each_available_child_of_node(pdev->dev.of_node, node) { edge = &smd->edges[i++]; edge->smd = smd; + init_waitqueue_head(&edge->new_channel_event); ret = qcom_smd_parse_edge(&pdev->dev, node, edge); if (ret) continue; - edge->need_rescan = true; - schedule_work(&edge->work); + schedule_work(&edge->scan_work); } platform_set_drvdata(pdev, smd); @@ -1332,8 +1445,10 @@ static int qcom_smd_remove(struct platform_device *pdev) edge = &smd->edges[i]; disable_irq(edge->irq); - cancel_work_sync(&edge->work); + cancel_work_sync(&edge->scan_work); + cancel_work_sync(&edge->state_work); + /* No need to lock here, because the writer is gone */ list_for_each_entry(channel, &edge->channels, list) { if (!channel->qsdev) continue; diff --git a/drivers/soc/qcom/spm.c b/drivers/soc/qcom/spm.c index 5548a31..f324451 100644 --- a/drivers/soc/qcom/spm.c +++ b/drivers/soc/qcom/spm.c @@ -2,6 +2,8 @@ * Copyright (c) 2011-2014, The Linux Foundation. All rights reserved. * Copyright (c) 2014,2015, Linaro Ltd. * + * SAW power controller driver + * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 and * only version 2 as published by the Free Software Foundation. @@ -12,7 +14,6 @@ * GNU General Public License for more details. */ -#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/io.h> @@ -378,8 +379,5 @@ static struct platform_driver spm_driver = { .of_match_table = spm_match_table, }, }; -module_platform_driver(spm_driver); -MODULE_LICENSE("GPL v2"); -MODULE_DESCRIPTION("SAW power controller driver"); -MODULE_ALIAS("platform:saw"); +builtin_platform_driver(spm_driver); diff --git a/drivers/soc/rockchip/pm_domains.c b/drivers/soc/rockchip/pm_domains.c index 43155e1..ac729fe 100644 --- a/drivers/soc/rockchip/pm_domains.c +++ b/drivers/soc/rockchip/pm_domains.c @@ -19,6 +19,7 @@ #include <linux/mfd/syscon.h> #include <dt-bindings/power/rk3288-power.h> #include <dt-bindings/power/rk3368-power.h> +#include <dt-bindings/power/rk3399-power.h> struct rockchip_domain_info { int pwr_mask; @@ -66,11 +67,11 @@ struct rockchip_pmu { #define DOMAIN(pwr, status, req, idle, ack) \ { \ - .pwr_mask = BIT(pwr), \ - .status_mask = BIT(status), \ - .req_mask = BIT(req), \ - .idle_mask = BIT(idle), \ - .ack_mask = BIT(ack), \ + .pwr_mask = (pwr >= 0) ? BIT(pwr) : 0, \ + .status_mask = (status >= 0) ? BIT(status) : 0, \ + .req_mask = (req >= 0) ? BIT(req) : 0, \ + .idle_mask = (idle >= 0) ? BIT(idle) : 0, \ + .ack_mask = (ack >= 0) ? BIT(ack) : 0, \ } #define DOMAIN_RK3288(pwr, status, req) \ @@ -79,6 +80,9 @@ struct rockchip_pmu { #define DOMAIN_RK3368(pwr, status, req) \ DOMAIN(pwr, status, req, (req) + 16, req) +#define DOMAIN_RK3399(pwr, status, req) \ + DOMAIN(pwr, status, req, req, req) + static bool rockchip_pmu_domain_is_idle(struct rockchip_pm_domain *pd) { struct rockchip_pmu *pmu = pd->pmu; @@ -96,6 +100,9 @@ static int rockchip_pmu_set_idle_request(struct rockchip_pm_domain *pd, struct rockchip_pmu *pmu = pd->pmu; unsigned int val; + if (pd_info->req_mask == 0) + return 0; + regmap_update_bits(pmu->regmap, pmu->info->req_offset, pd_info->req_mask, idle ? -1U : 0); @@ -116,6 +123,10 @@ static bool rockchip_pmu_domain_is_on(struct rockchip_pm_domain *pd) struct rockchip_pmu *pmu = pd->pmu; unsigned int val; + /* check idle status for idle-only domains */ + if (pd->info->status_mask == 0) + return !rockchip_pmu_domain_is_idle(pd); + regmap_read(pmu->regmap, pmu->info->status_offset, &val); /* 1'b0: power on, 1'b1: power off */ @@ -127,6 +138,9 @@ static void rockchip_do_pmu_set_power_domain(struct rockchip_pm_domain *pd, { struct rockchip_pmu *pmu = pd->pmu; + if (pd->info->pwr_mask == 0) + return; + regmap_update_bits(pmu->regmap, pmu->info->pwr_offset, pd->info->pwr_mask, on ? 0 : -1U); @@ -360,6 +374,61 @@ static void rockchip_configure_pd_cnt(struct rockchip_pmu *pmu, regmap_write(pmu->regmap, domain_reg_offset + 4, count); } +static int rockchip_pm_add_subdomain(struct rockchip_pmu *pmu, + struct device_node *parent) +{ + struct device_node *np; + struct generic_pm_domain *child_domain, *parent_domain; + int error; + + for_each_child_of_node(parent, np) { + u32 idx; + + error = of_property_read_u32(parent, "reg", &idx); + if (error) { + dev_err(pmu->dev, + "%s: failed to retrieve domain id (reg): %d\n", + parent->name, error); + goto err_out; + } + parent_domain = pmu->genpd_data.domains[idx]; + + error = rockchip_pm_add_one_domain(pmu, np); + if (error) { + dev_err(pmu->dev, "failed to handle node %s: %d\n", + np->name, error); + goto err_out; + } + + error = of_property_read_u32(np, "reg", &idx); + if (error) { + dev_err(pmu->dev, + "%s: failed to retrieve domain id (reg): %d\n", + np->name, error); + goto err_out; + } + child_domain = pmu->genpd_data.domains[idx]; + + error = pm_genpd_add_subdomain(parent_domain, child_domain); + if (error) { + dev_err(pmu->dev, "%s failed to add subdomain %s: %d\n", + parent_domain->name, child_domain->name, error); + goto err_out; + } else { + dev_dbg(pmu->dev, "%s add subdomain: %s\n", + parent_domain->name, child_domain->name); + } + + rockchip_pm_add_subdomain(pmu, np); + } + + return 0; + +err_out: + of_node_put(np); + return error; +} + static int rockchip_pm_domain_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; @@ -406,6 +475,10 @@ static int rockchip_pm_domain_probe(struct platform_device *pdev) } pmu->regmap = syscon_node_to_regmap(parent->of_node); + if (IS_ERR(pmu->regmap)) { + dev_err(dev, "no regmap available\n"); + return PTR_ERR(pmu->regmap); + } /* * Configure power up and down transition delays for CORE @@ -426,6 +499,14 @@ static int rockchip_pm_domain_probe(struct platform_device *pdev) of_node_put(node); goto err_out; } + + error = rockchip_pm_add_subdomain(pmu, node); + if (error < 0) { + dev_err(dev, "failed to handle subdomain node %s: %d\n", + node->name, error); + of_node_put(node); + goto err_out; + } } if (error) { @@ -457,6 +538,36 @@ static const struct rockchip_domain_info rk3368_pm_domains[] = { [RK3368_PD_GPU_1] = DOMAIN_RK3368(17, 16, 2), }; +static const struct rockchip_domain_info rk3399_pm_domains[] = { + [RK3399_PD_TCPD0] = DOMAIN_RK3399(8, 8, -1), + [RK3399_PD_TCPD1] = DOMAIN_RK3399(9, 9, -1), + [RK3399_PD_CCI] = DOMAIN_RK3399(10, 10, -1), + [RK3399_PD_CCI0] = DOMAIN_RK3399(-1, -1, 15), + [RK3399_PD_CCI1] = DOMAIN_RK3399(-1, -1, 16), + [RK3399_PD_PERILP] = DOMAIN_RK3399(11, 11, 1), + [RK3399_PD_PERIHP] = DOMAIN_RK3399(12, 12, 2), + [RK3399_PD_CENTER] = DOMAIN_RK3399(13, 13, 14), + [RK3399_PD_VIO] = DOMAIN_RK3399(14, 14, 17), + [RK3399_PD_GPU] = DOMAIN_RK3399(15, 15, 0), + [RK3399_PD_VCODEC] = DOMAIN_RK3399(16, 16, 3), + [RK3399_PD_VDU] = DOMAIN_RK3399(17, 17, 4), + [RK3399_PD_RGA] = DOMAIN_RK3399(18, 18, 5), + [RK3399_PD_IEP] = DOMAIN_RK3399(19, 19, 6), + [RK3399_PD_VO] = DOMAIN_RK3399(20, 20, -1), + [RK3399_PD_VOPB] = DOMAIN_RK3399(-1, -1, 7), + [RK3399_PD_VOPL] = DOMAIN_RK3399(-1, -1, 8), + [RK3399_PD_ISP0] = DOMAIN_RK3399(22, 22, 9), + [RK3399_PD_ISP1] = DOMAIN_RK3399(23, 23, 10), + [RK3399_PD_HDCP] = DOMAIN_RK3399(24, 24, 11), + [RK3399_PD_GMAC] = DOMAIN_RK3399(25, 25, 23), + [RK3399_PD_EMMC] = DOMAIN_RK3399(26, 26, 24), + [RK3399_PD_USB3] = DOMAIN_RK3399(27, 27, 12), + [RK3399_PD_EDP] = DOMAIN_RK3399(28, 28, 22), + [RK3399_PD_GIC] = DOMAIN_RK3399(29, 29, 27), + [RK3399_PD_SD] = DOMAIN_RK3399(30, 30, 28), + [RK3399_PD_SDIOAUDIO] = DOMAIN_RK3399(31, 31, 29), +}; + static const struct rockchip_pmu_info rk3288_pmu = { .pwr_offset = 0x08, .status_offset = 0x0c, @@ -491,6 +602,23 @@ static const struct rockchip_pmu_info rk3368_pmu = { .domain_info = rk3368_pm_domains, }; +static const struct rockchip_pmu_info rk3399_pmu = { + .pwr_offset = 0x14, + .status_offset = 0x18, + .req_offset = 0x60, + .idle_offset = 0x64, + .ack_offset = 0x68, + + .core_pwrcnt_offset = 0x9c, + .gpu_pwrcnt_offset = 0xa4, + + .core_power_transition_time = 24, + .gpu_power_transition_time = 24, + + .num_domains = ARRAY_SIZE(rk3399_pm_domains), + .domain_info = rk3399_pm_domains, +}; + static const struct of_device_id rockchip_pm_domain_dt_match[] = { { .compatible = "rockchip,rk3288-power-controller", @@ -500,6 +628,10 @@ static const struct of_device_id rockchip_pm_domain_dt_match[] = { .compatible = "rockchip,rk3368-power-controller", .data = (void *)&rk3368_pmu, }, + { + .compatible = "rockchip,rk3399-power-controller", + .data = (void *)&rk3399_pmu, + }, { /* sentinel */ }, }; |