diff --git a/drivers/gpu/drm/tegra/gr3d.c b/drivers/gpu/drm/tegra/gr3d.c
index b0b8154e8104..11c38af584ee 100644
--- a/drivers/gpu/drm/tegra/gr3d.c
+++ b/drivers/gpu/drm/tegra/gr3d.c
@@ -10,8 +10,12 @@
 #include <linux/module.h>
 #include <linux/of_device.h>
 #include <linux/platform_device.h>
+#include <linux/pm_domain.h>
+#include <linux/pm_opp.h>
+#include <linux/pm_runtime.h>
 #include <linux/reset.h>
 
+#include <soc/tegra/common.h>
 #include <soc/tegra/pmc.h>
 
 #include "drm.h"
@@ -31,6 +35,9 @@ struct gr3d {
 	struct reset_control *rst;
 
 	const struct gr3d_soc *soc;
+	struct clk_bulk_data clocks[2];
+	unsigned int nclocks;
+	bool legacy_pd;
 
 	DECLARE_BITMAP(addr_regs, GR3D_NUM_REGS);
 };
@@ -278,10 +285,120 @@ static const u32 gr3d_addr_regs[] = {
 	GR3D_GLOBAL_SAMP23SURFADDR(15),
 };
 
+static void gr3d_pm_runtime_release(void *dev)
+{
+	pm_runtime_put(dev);
+	pm_runtime_disable(dev);
+}
+
+static int gr3d_link_power_domain(struct device *dev, struct device *pd_dev)
+{
+	const u32 link_flags = DL_FLAG_STATELESS | DL_FLAG_PM_RUNTIME;
+	struct device_link *link;
+	int err;
+
+	link = device_link_add(dev, pd_dev, link_flags);
+	if (!link) {
+		dev_err(dev, "failed to link to %s\n", dev_name(pd_dev));
+		return -EINVAL;
+	}
+
+	err = devm_add_action_or_reset(dev, (void *)device_link_del, link);
+	if (err)
+		return err;
+
+	return 0;
+}
+
+static int devm_gr3d_init_power(struct device *dev, struct gr3d *gr3d)
+{
+	const char *opp_genpd_names[] = { "3d0", "3d1", NULL };
+	struct device **opp_virt_dev;
+	struct opp_table *opp_table;
+	unsigned int i, num_domains;
+	struct device *pd_dev;
+	int err;
+
+	err = of_count_phandle_with_args(dev->of_node, "power-domains",
+					 "#power-domain-cells");
+	if (err < 0) {
+		if (err != -ENOENT)
+			return err;
+
+		/*
+		 * Older device-trees don't use GENPD. In this case we should
+		 * toggle power domain manually.
+		 */
+		gr3d->legacy_pd = true;
+		goto power_up;
+	}
+
+	num_domains = err;
+
+	/*
+	 * The PM domain core automatically attaches a single power domain,
+	 * otherwise it skips attaching completely. We have a single domain
+	 * on Tegra20 and two domains on Tegra30+.
+	 */
+	if (dev->pm_domain)
+		goto power_up;
+
+	opp_table = devm_pm_opp_attach_genpd(dev, opp_genpd_names, &opp_virt_dev);
+	if (IS_ERR(opp_table))
+		return PTR_ERR(opp_table);
+
+	for (i = 0; opp_genpd_names[i]; i++) {
+		pd_dev = opp_virt_dev[i];
+		if (!pd_dev) {
+			dev_err(dev, "failed to get %s power domain\n",
+				opp_genpd_names[i]);
+			return -EINVAL;
+		}
+
+		err = gr3d_link_power_domain(dev, pd_dev);
+		if (err)
+			return err;
+	}
+
+power_up:
+	pm_runtime_enable(dev);
+	err = pm_runtime_get_sync(dev);
+	if (err < 0) {
+		gr3d_pm_runtime_release(dev);
+		return err;
+	}
+
+	err = devm_add_action_or_reset(dev, gr3d_pm_runtime_release, dev);
+	if (err)
+		return err;
+
+	return 0;
+}
+
+static int gr3d_set_opp(struct dev_pm_set_opp_data *data)
+{
+	struct gr3d *gr3d = dev_get_drvdata(data->dev);
+	unsigned int i;
+	int err;
+
+	for (i = 0; i < gr3d->nclocks; i++) {
+		err = clk_set_rate(gr3d->clocks[i].clk, data->new_opp.rate);
+		if (err) {
+			dev_err(data->dev, "failed to set %s rate to %lu: %d\n",
+				gr3d->clocks[i].id, data->new_opp.rate, err);
+			return err;
+		}
+	}
+
+	return 0;
+}
+
 static int gr3d_probe(struct platform_device *pdev)
 {
+	struct tegra_core_opp_params opp_params = {};
 	struct device_node *np = pdev->dev.of_node;
 	struct host1x_syncpt **syncpts;
+	struct opp_table *opp_table;
 	struct gr3d *gr3d;
 	unsigned int i;
 	int err;
@@ -290,6 +407,8 @@ static int gr3d_probe(struct platform_device *pdev)
 	if (!gr3d)
 		return -ENOMEM;
 
+	platform_set_drvdata(pdev, gr3d);
+
 	gr3d->soc = of_device_get_match_data(&pdev->dev);
 
 	syncpts = devm_kzalloc(&pdev->dev, sizeof(*syncpts), GFP_KERNEL);
@@ -302,7 +421,11 @@ static int gr3d_probe(struct platform_device *pdev)
 		return PTR_ERR(gr3d->clk);
 	}
 
-	gr3d->rst = devm_reset_control_get(&pdev->dev, "3d");
+	gr3d->clocks[gr3d->nclocks].id = "3d";
+	gr3d->clocks[gr3d->nclocks].clk = gr3d->clk;
+	gr3d->nclocks++;
+
+	gr3d->rst = devm_reset_control_get_exclusive_released(&pdev->dev, "3d");
 	if (IS_ERR(gr3d->rst)) {
 		dev_err(&pdev->dev, "cannot get reset\n");
 		return PTR_ERR(gr3d->rst);
@@ -315,31 +438,31 @@ static int gr3d_probe(struct platform_device *pdev)
 			return PTR_ERR(gr3d->clk_secondary);
 		}
 
-		gr3d->rst_secondary = devm_reset_control_get(&pdev->dev,
-								"3d2");
+		gr3d->clocks[gr3d->nclocks].id = "3d2";
+		gr3d->clocks[gr3d->nclocks].clk = gr3d->clk_secondary;
+		gr3d->nclocks++;
+
+		gr3d->rst_secondary =
+			devm_reset_control_get_exclusive_released(&pdev->dev, "3d2");
 		if (IS_ERR(gr3d->rst_secondary)) {
 			dev_err(&pdev->dev, "cannot get secondary reset\n");
 			return PTR_ERR(gr3d->rst_secondary);
 		}
 	}
 
-	err = tegra_powergate_sequence_power_up(TEGRA_POWERGATE_3D, gr3d->clk,
-						gr3d->rst);
-	if (err < 0) {
-		dev_err(&pdev->dev, "failed to power up 3D unit\n");
+	err = devm_gr3d_init_power(&pdev->dev, gr3d);
+	if (err)
 		return err;
-	}
 
-	if (gr3d->clk_secondary) {
-		err = tegra_powergate_sequence_power_up(TEGRA_POWERGATE_3D1,
-							gr3d->clk_secondary,
-							gr3d->rst_secondary);
-		if (err < 0) {
-			dev_err(&pdev->dev,
-				"failed to power up secondary 3D unit\n");
-			return err;
-		}
-	}
+	opp_table = devm_pm_opp_register_set_opp_helper(&pdev->dev, gr3d_set_opp);
+	if (IS_ERR(opp_table))
+		return PTR_ERR(opp_table);
+
+	opp_params.init_state = true;
+
+	err = devm_tegra_core_dev_init_opp_table(&pdev->dev, &opp_params);
+	if (err && err != -ENODEV)
+		return err;
 
 	INIT_LIST_HEAD(&gr3d->client.base.list);
 	gr3d->client.base.ops = &gr3d_client_ops;
@@ -363,8 +486,6 @@ static int gr3d_probe(struct platform_device *pdev)
 	for (i = 0; i < ARRAY_SIZE(gr3d_addr_regs); i++)
 		set_bit(gr3d_addr_regs[i], gr3d->addr_regs);
 
-	platform_set_drvdata(pdev, gr3d);
-
 	return 0;
 }
 
@@ -380,23 +501,114 @@ static int gr3d_remove(struct platform_device *pdev)
 		return err;
 	}
 
-	if (gr3d->clk_secondary) {
-		reset_control_assert(gr3d->rst_secondary);
+	return 0;
+}
+
+static int __maybe_unused gr3d_runtime_suspend(struct device *dev)
+{
+	struct gr3d *gr3d = dev_get_drvdata(dev);
+	int err;
+
+	if (gr3d->legacy_pd && gr3d->clk_secondary) {
+		err = reset_control_assert(gr3d->rst_secondary);
+		if (err) {
+			dev_err(dev, "failed to assert secondary reset: %d\n", err);
+			return err;
+		}
+
 		tegra_powergate_power_off(TEGRA_POWERGATE_3D1);
-		clk_disable_unprepare(gr3d->clk_secondary);
 	}
 
-	reset_control_assert(gr3d->rst);
-	tegra_powergate_power_off(TEGRA_POWERGATE_3D);
-	clk_disable_unprepare(gr3d->clk);
+	if (gr3d->legacy_pd) {
+		err = reset_control_assert(gr3d->rst);
+		if (err) {
+			dev_err(dev, "failed to assert reset: %d\n", err);
+			return err;
+		}
+
+		tegra_powergate_power_off(TEGRA_POWERGATE_3D);
+	}
+
+	clk_bulk_disable_unprepare(gr3d->nclocks, gr3d->clocks);
+	reset_control_release(gr3d->rst_secondary);
+	reset_control_release(gr3d->rst);
+
+	return 0;
+}
+
+static int __maybe_unused gr3d_runtime_resume(struct device *dev)
+{
+	struct gr3d *gr3d = dev_get_drvdata(dev);
+	int err;
+
+	err = reset_control_acquire(gr3d->rst);
+	if (err) {
+		dev_err(dev, "failed to acquire reset: %d\n", err);
+		return err;
+	}
+
+	err = reset_control_acquire(gr3d->rst_secondary);
+	if (err) {
+		dev_err(dev, "failed to acquire secondary reset: %d\n", err);
+		goto release_reset_primary;
+	}
+
+	if (gr3d->legacy_pd) {
+		err = tegra_powergate_sequence_power_up(TEGRA_POWERGATE_3D,
+							gr3d->clk, gr3d->rst);
+		if (err)
+			goto release_reset_secondary;
+	}
+
+	if (gr3d->legacy_pd && gr3d->clk_secondary) {
+		err = tegra_powergate_sequence_power_up(TEGRA_POWERGATE_3D1,
+							gr3d->clk_secondary,
+							gr3d->rst_secondary);
+		if (err)
+			goto release_reset_secondary;
+	}
+
+	err = clk_bulk_prepare_enable(gr3d->nclocks, gr3d->clocks);
+	if (err) {
+		dev_err(dev, "failed to enable clock: %d\n", err);
+		goto release_reset_secondary;
+	}
+
+	return 0;
+
+release_reset_secondary:
+	reset_control_release(gr3d->rst_secondary);
+
+release_reset_primary:
+	reset_control_release(gr3d->rst);
+
+	return err;
+}
+
+static __maybe_unused int gr3d_suspend(struct device *dev)
+{
+	struct gr3d *gr3d = dev_get_drvdata(dev);
+	int err;
+
+	host1x_channel_stop(gr3d->channel);
+
+	err = pm_runtime_force_suspend(dev);
+	if (err < 0)
+		return err;
 
 	return 0;
 }
 
+static const struct dev_pm_ops tegra_gr3d_pm = {
+	SET_RUNTIME_PM_OPS(gr3d_runtime_suspend, gr3d_runtime_resume, NULL)
+	SET_SYSTEM_SLEEP_PM_OPS(gr3d_suspend, pm_runtime_force_resume)
+};
+
 struct platform_driver tegra_gr3d_driver = {
 	.driver = {
 		.name = "tegra-gr3d",
 		.of_match_table = tegra_gr3d_match,
+		.pm = &tegra_gr3d_pm,
 	},
 	.probe = gr3d_probe,
 	.remove = gr3d_remove,
